一天一模式之21装饰器模式

原博文:https://blog.csdn.net/KongZhongNiao/article/details/80704184

学习装饰模式

一:初识装饰模式

包括:定义、结构、参考实现

二:体会装饰模式

包括:场景问题、不用模式的解决方案、使用模式的解决方案

三:理解装饰模式

包括:认识装饰模式、Java中的装饰模式应用、装饰模式和AOP 、
装饰模式的优缺点

四:思考装饰模式

包括:装饰模式的本质、何时选用

初识装饰模式

定义

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

结构和说明

Component:

组件对象的接口,可以给这些对象动态的添加职责。

ConcreteComponent:

具体的组件对象,实现组件对象接口,通常就是被装饰器装饰的原始对
象,也就是可以给这个对象添加职责。

Decorator:

所有装饰器的抽象父类,需要定义一个与组件接口一致的接口,并持有一
个Component对象,其实就是持有一个被装饰的对象。

注意这个被装饰的对象不一定是最原始的那个对象了,也可能是被其它装
饰器装饰过后的对象,反正都是实现的同一个接口,也就是同一类型。

ConcreteDecorator:

实际的装饰器对象,实现具体要向被装饰对象添加的功能。

示例
组件对象的接口,可以给这些对象动态的添加职责
package cn.javass.dp.decorator.example3;

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

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
装饰器接口,维持一个指向组件对象的接口对象,
package cn.javass.dp.decorator.example3;

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

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

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


}
 
 
  • 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
具体实现组件对象接口的对象
package cn.javass.dp.decorator.example3;

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

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

}


 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
装饰器的具体实现对象,向组件对象添加职责
package cn.javass.dp.decorator.example3;

/**
 * 装饰器的具体实现对象,向组件对象添加职责
 */
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---------->");
    }
}


 
 
  • 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
装饰器的具体实现对象,向组件对象添加职责
package cn.javass.dp.decorator.example3;

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


 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

体会装饰模式

复杂的奖金计算

奖金计算是相对复杂的功能,尤其是对于业务部门的奖金计算方式,是非
常复杂的,除了业务功能复杂外,另外一个麻烦之处是计算方式还经常需要变
动,因为业务部门经常通过调整奖金的计算方式来激励士气。

先从业务上看看现有的奖金计算方式的复杂性:

  • 1:首先是奖金分类:对于个人,大致有个人当月业务奖金、个人累计奖金、个人业
    务增长奖金、及时回款奖金、限时成交加码奖金等等;
  • 2:对于业务主管或者是业务经理,除了个人奖金外,还有:团队累计奖金、团队业
    务增长奖金、团队盈利奖金等等。
  • 3:其次是计算奖金的金额,又有这么几个基数:销售额、销售毛利、实际回款、业
    务成本、奖金基数等等;
  • 4:另外一个就是计算的公式,针对不同的人、不同的奖金类别、不同的计算奖金的
    金额,计算的公式是不同的,就算是同一个公式,里面计算的比例参数也有可能
    是不同的。
简化后的奖金计算体系

看了上面奖金计算的问题,所幸我们只是来学习设计模式,并不是真的要
去实现整个奖金计算体系的业务,因此也没有必要把所有的计算业务都罗列在这
里,为了后面演示的需要,简化一下,演示用的奖金计算体系如下:
- 1:每个人当月业务奖金 = 当月销售额 X 3%
- 2:每个人累计奖金 = 总的回款额 X 0.1%
- 3:团队奖金 = 团队总销售额 X 1%

不用模式的解决方案

示例
在内存中模拟数据库,准备点测试数据,好计算奖金
package cn.javass.dp.decorator.example1;
import java.util.*;
/**
 * 在内存中模拟数据库,准备点测试数据,好计算奖金
 */
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);
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
计算奖金的对象
package cn.javass.dp.decorator.example1;

import java.util.Date;
/**
 * 计算奖金的对象
 */
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;
    }
}

 
 
  • 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
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
客户端
package cn.javass.dp.decorator.example1;

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);
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

有何问题

对于奖金计算,光是计算方式复杂,也就罢了,不过是实现起来会困难

点,相对而言还是比较好解决的,不过是用程序把已有的算法表达出来。
最痛苦的是,这些奖金的计算方式,经常发生变动,几乎是每个季度都会

有小调整,每年都有大调整,这就要求软件的实现要足够灵活,要能够很快进行
相应调整和修改,否则就不能满足实际业务的需要。
举个简单的例子来说,现在根据业务需要,需要增加一个“环比增长奖

金”,就是本月的销售额比上个月有增加,而且要达到一定的比例,当然增长比
例越高,奖金比例越大。那么软件就必须要重新实现这么个功能,并正确的添加
到系统中去。过了两个月,业务奖励的策略发生了变化,不再需要这个奖金了,
或者是另外换了一个新的奖金方式了,那么软件就需要把这个功能从软件中去
掉,然后再实现新的功能。

那么上面的要求该如何实现呢?

很明显,一种方案是通过继承来扩展功能;另外一种方案就是到计算奖金
的对象里面,添加或者删除新的功能,并在计算奖金的时候,调用新的功能或是
不调用某些去掉的功能,这种方案会严重违反开-闭原则。

还有一个问题,就是在运行期间,不同人员参与的奖金计算方式也是不同
的,举例来说:如果是业务经理,除了参与个人计算部分外,还要参加团队奖金
的计算,这就意味着需要在运行期间动态来组合需要计算的部分,也就是会有一
堆的if-else。

总结一下,奖金计算面临如下问题:
  • (1)计算逻辑复杂
  • (2)要有足够灵活性,可以方便的增加或者减少功能
  • (3)要能动态的组合计算方式,不同的人参与的计算不同

上面描述的奖金计算的问题,绝对没有任何夸大成分,相反已经简化不少
了,还有更多麻烦没有写上来,毕竟我们的重点在设计模式,而不是业务。

把上面的问题抽象一下,设若有一个计算奖金的对象,现在需要能够灵活
的给它增加和减少功能,还需要能够动态的组合功能,每个功能就相当于在计算
奖金的某个部分。

现在的问题就是:如何才能够透明的给一个对象增加功能,并实现功能的
动态组合呢?

使用模式来解决的思路

虽然经过简化,业务简单了很多,但是需要解决的问题不会少,还是要解

决:要透明的给一个对象增加功能,并实现功能的动态组合。

所谓透明的给一个对象增加功能,换句话说就是要给一个对象增加功能,
但是不能让这个对象知道,也就是不能去改动这个对象。而实现了能够给一个对
象透明的增加功能,自然就能够实现功能的动态组合。

在装饰模式的实现中,为了能够和原来使用被装饰对象的代码实现无缝结
合,是通过定义一个抽象类,让这个类实现与被装饰对象相同的接口,然后在具
体实现类里面,转调被装饰的对象,在转调的前后添加新的功能,这就实现了给
被装饰对象增加功能,这个思路跟“对象组合”非常类似。

在转调的时候,如果觉得被装饰的对象的功能不再需要了,还可以直接替
换掉,也就是不再转调,而是在装饰对象里面完全全新的实现。

使用模式来解决的类图

画个图来说明奖金的计算过程

这个图很好的揭示了装饰模式的组合和调用过程,请仔细体会一下。

示例
在内存中模拟数据库,准备点测试数据,好计算奖金
package cn.javass.dp.decorator.example4;
import java.util.*;
/**
 * 在内存中模拟数据库,准备点测试数据,好计算奖金
 */
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);
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
装饰器的接口,需要跟被装饰的对象实现同样的接口
package cn.javass.dp.decorator.example4;

import java.util.Date;

/**
 * 装饰器的接口,需要跟被装饰的对象实现同样的接口
 */
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);
    }
}

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

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
基本的实现计算奖金的类,也是被装饰器装饰的对象
package cn.javass.dp.decorator.example4;

import java.util.Date;

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

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

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
装饰器对象,计算当月团队业务奖金
package cn.javass.dp.decorator.example4;

import java.util.Date;
/**
 * 装饰器对象,计算当月团队业务奖金
 */
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;
    }

}

 
 
  • 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
装饰器对象,计算累计奖金
package cn.javass.dp.decorator.example4;

import java.util.Date;
/**
 * 装饰器对象,计算累计奖金
 */
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;
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
装饰器对象,计算当月业务奖金
package cn.javass.dp.decorator.example4;

import java.util.Date;
/**
 * 装饰器对象,计算当月业务奖金
 */
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;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
客户端
package cn.javass.dp.decorator.example4;

/**
 * 使用装饰模式的客户端
 */
public class Client {
    public static void main(String[] args) {
        //先创建计算基本奖金的类,这也是被装饰的对象
        Component c1 = new ConcreteComponent();

        //然后对计算的基本奖金进行装饰,这里要组合各个装饰
        //说明,各个装饰者之间最好是不要有先后顺序的限制,也就是先装饰谁和后装饰谁都应该是一样的

        //先组合普通业务人员的奖金计算
        Decorator d1 = new MonthPrizeDecorator(c1);
        Decorator d2 = new SumPrizeDecorator(d1);   

        //注意:这里只需要使用最后组合好的对象调用业务方法即可,会依次调用回去
        //日期对象都没有用上,所以传null就可以了

        Decorator d4 = new MonthPrizeDecorator(d2);

        double zs = d4.calcPrize("张三",null,null);       
        System.out.println("==========张三应得奖金:"+zs);
        double ls = d2.calcPrize("李四",null,null);
        System.out.println("==========李四应得奖金:"+ls);

        //如果是业务经理,还需要一个计算团队的奖金计算
        Decorator d3 = new GroupPrizeDecorator(d2);
        double ww = d3.calcPrize("王五",null,null);
        System.out.println("==========王经理应得奖金:"+ww);

    }
}
 
 
  • 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

理解装饰模式

认识装饰模式

1:装饰模式的功能

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

变相的还得到了另外一个好处,那就是装饰器功能的复用,可以给一个对
象多次增加同一个装饰器,也可以用同一个装饰器装饰不同的对象。

2:对象组合

在面向对象设计中,有一条很基本的规则就是“尽量使用对象组合,而不
是对象继承”来扩展和复用功能 ,装饰模式的思考起点就是这个规则 。

3:装饰器

装饰器实现了对被装饰对象的某些装饰功能,可以在装饰器里面调用被装
饰对象的功能,获取相应的值,这其实是一种递归调用。

在装饰器里不仅仅是可以给被装饰对象增加功能,还可以根据需要选择是
否调用被装饰对象的功能,如果不调用被装饰对象的功能,那就变成完全重新实
现了,相当于动态修改了被装饰对象的功能。

另外一点,各个装饰器之间最好是完全独立的功能,不要有依赖,这样在
进行装饰组合的时候,才没有先后顺序的限制,也就是先装饰谁和后装饰谁都应
该是一样的,否则会大大降低装饰器组合的灵活性。

4:装饰器和组件类的关系

装饰器是用来装饰组件的,装饰器一定要实现和组件类一致的接口,保证
它们是同一个类型,并具有同一个外观,这样组合完成的装饰才能够递归的调用
下去。

组件类是不知道装饰器的存在的,装饰器给组件添加功能是一种透明的包
装,组件类毫不知情。需要改变的是外部使用组件类的地方,现在需要使用包装
后的类,接口是一样的,但是具体的实现类发生了改变。

示例
A
package cn.javass.dp.decorator.example2;

public class A {
    public void a1(){
        System.out.println("now in A.a1");
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
B
package cn.javass.dp.decorator.example2;

public class B {
    public void b1(){
        System.out.println("now in B.b1");
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
C1 extends A
package cn.javass.dp.decorator.example2;

public class C1 extends A{
    public void c11(){
        System.out.println("now in C1.c11");
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
C2 内部实现组合
package cn.javass.dp.decorator.example2;

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");
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
C3 内部实现组合
package cn.javass.dp.decorator.example2;

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");
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
示例直接在属性上创建需要组合的对象
package cn.javass.dp.decorator.example2;

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");
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
5:退化形式

如果仅仅只是想要添加一个功能,就没有必要再设计装饰器的抽象类了,
直接在装饰器里面实现跟组件一样的接口,然后实现相应的装饰功能就可以了。
但是建议最好还是设计上装饰器的抽象类,这样有利于程序的扩展。

Java中的装饰模式应用

Java中典型的装饰模式应用——I/O流

Java的I/O对象层次图

查看上图会发现,它的结构和装饰模式的结构几乎是一样的:
  • 1:InputStream就相当于装饰模式中的Component。
  • 2:其实FileInputStream、ObjectInputStream、StringBufferInputStream这几个
    对象是直接继承了InputSream,还有几个直接继承InputStream的对象,比如:
    ByteArrayInputStream、PipedInputStream等。这些对象相当于装饰模式中的
    ConcreteComponent,是可以被装饰器装饰的对象。
  • 3:那么FilterInputStream就相当于装饰模式中的Decorator,而它的子类
    DataInputStream、BufferedInputStream、LineNumberInputStream和
    PushbackInputStream就相当于装饰模式中的ConcreteDecorator了。另外
    FilterInputStream和它的子类对象的构造器,都是传入组件InputStream类型,
    这样就完全符合前面讲述的装饰器的结构了。
自己实现的I/O流的装饰器——第一版

来个功能简单点的,实现把英文加密存放吧,也谈不上什么加密算法,就
是把英文字母向后移动两个位置,比如:a变成c,b变成d,以此类推,最后的y
变成a,z就变成b,而且为了简单,只处理小写的,够简单的吧。

示例
IOTest
package cn.javass.dp.decorator.example5;
import java.io.*;
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();
        }       
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
实现简单的加密
package cn.javass.dp.decorator.example5;

import java.io.*;

/**
 * 实现简单的加密
 */
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
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
客户端
package cn.javass.dp.decorator.example5;

import java.io.*;

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();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

测试中可能会出现输出一片空白,要把这个问题搞清楚,就需要把上面I/O
流的内部运行和基本实现搞明白,分开来看看具体的运行过程吧。

示例
package cn.javass.dp.decorator.example5;

import java.io.*;

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("abcdxy".getBytes());
        dout.close();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
先看看成功输出流中的内容的写法的运行过程:
  • 1:当执行到“dout.write(”abcdxyz“.getBytes());”这句话的时候,会调用
    DataOutputStream的write方法,把数据输出到BufferedOutputStream中;
  • 2:由于BufferedOutputStream流是一个带缓存的流,它默认缓存8192byte,也就是
    默认流中的缓存数据到了8192byte,它才会自动输出缓存中的数据;
  • 3:而目前要输出的字节肯定不到8192byte,因此数据就被缓存在
    BufferedOutputStream流中了,而不会被自动输出

  • 4:当执行到“dout.close();”这句话的时候:会调用关闭DataOutputStream流,
    这会转调到传入DataOutputStream中的流的close方法,也就是
    BufferedOutputStream的close方法,而BufferedOutputStream的close方法继承
    自FilterOutputStream,在FilterOutputStream的close方法实现里面,会先调
    用输出流的方法flush,然后关闭流。也就是此时BufferedOutputStream流中缓
    存的数据会被强制输出;

  • 5:BufferedOutputStream流中缓存的数据被强制输出到EncryptOutputStream流,
    也就我们自己实现的流,没有缓存,经过处理后继续输出;
  • 6:EncryptOutputStream流会把数据输出到FileOutputStream中,
    FileOutputStream会直接把数据输出到文件中,因此,这种实现方式会输出文件
    的内容。
再来看看不能输出流中的内容的写法的运行过程:
  • 1:当执行到“dout.write(”abcdxyz“.getBytes());”这句话的时候,会调用
    DataOutputStream的write方法,把数据输出到EncryptOutputStream中;
  • 2:EncryptOutputStream流,也就是我们自己实现的流,没有缓存,经过处理后继续输
    出,把数据输出到BufferedOutputStream中;
  • 3:由于BufferedOutputStream流是一个带缓存的流,它默认缓存8192byte,也就是默认
    流中的缓存数据到了8192byte,它才会自动输出缓存中的数据;
  • 4:而目前要输出的字节肯定不到8192byte,因此数据就被缓存在BufferedOutputStream
    流中了,而不会被自动输出
  • 5:当执行到“dout.close();”这句话的时候:会调用关闭DataOutputStream流,这会
    转调到传入DataOutputStream流中的流的close方法,也就是EncryptOutputStream的
    close方法,而EncryptOutputStream的close方法继承自OutputStream,在
    OutputStream的close方法实现里面,是个空方法,什么都没有做。因此,这种实现
    方式没有flush流的数据,也就不会输出文件的内容,自然是一片空白了。
自己实现的I/O流的装饰器——第二版

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

示例
实现简单的加密
package cn.javass.dp.decorator.example5;

import java.io.*;

/**
 * 实现简单的加密
 */
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);
    }
}

 
 
  • 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
客户端
package cn.javass.dp.decorator.example5;

import java.io.*;

public class Client {
    public static void main(String[] args) throws Exception {
        //流式输出文件
        DataOutputStream dout = 
        new DataOutputStream(
                new EncryptOutputStream2(
                new BufferedOutputStream(

                    new FileOutputStream("MyEncrypt.txt"))));
        //然后就可以输出内容了
        dout.write("abcdxy".getBytes());
        dout.close();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

装饰模式和AOP

关于AOP的知识,可以参看私塾在线上关于AOP的课程,或者是Spring的课程。

可以使用装饰模式做出类似AOP的效果

示例
商品销售管理的业务接口
package cn.javass.dp.decorator.example6;
/**
 * 商品销售管理的业务接口
 */
public interface GoodsSaleEbi {
    /**
     * 保存销售信息,本来销售数据应该是多条,太麻烦了,为了演示,简单点
     * @param user 操作人员
     * @param customer 客户
     * @param saleModel 销售数据
     * @return 是否保存成功
     */
    public boolean sale(String user,String customer,SaleModel saleModel);
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
实现商品销售管理的业务
package cn.javass.dp.decorator.example6;

public class GoodsSaleEbo implements GoodsSaleEbi{

    public boolean sale(String user,String customer, SaleModel saleModel) {
        System.out.println(user+"保存了"+customer+"购买 "+saleModel+" 的销售数据");
        return true;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
装饰器的接口,需要跟被装饰的对象实现同样的接口
package cn.javass.dp.decorator.example6;
/**
 * 装饰器的接口,需要跟被装饰的对象实现同样的接口
 */
public abstract class Decorator implements GoodsSaleEbi{
    /**
     * 持有被装饰的组件对象
     */
    protected GoodsSaleEbi ebi;
    /**
     * 通过构造方法传入被装饰的对象
     * @param ebi被装饰的对象
     */
    public Decorator(GoodsSaleEbi ebi){
        this.ebi = ebi;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
实现日志记录
package cn.javass.dp.decorator.example6;
import java.text.*;
import java.util.Date;
/**
 * 实现日志记录
 */
public class LogDecorator extends Decorator{
    public LogDecorator(GoodsSaleEbi ebi){
        super(ebi);
    }

    public boolean sale(String user,String customer, SaleModel saleModel) {
        //执行业务功能
        boolean f = this.ebi.sale(user, customer, saleModel);
        //在执行业务功能过后,记录日志
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        System.out.println("日志记录:"+user+"于"+df.format(new Date())+"时保存了一条销售记录,客户是"+customer+",购买记录是"+saleModel);
        return f;
    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
实现权限控制
package cn.javass.dp.decorator.example6;
/**
 * 实现权限控制
 */
public class CheckDecorator extends Decorator{
    public CheckDecorator(GoodsSaleEbi ebi){
        super(ebi);
    }

    public boolean sale(String user,String customer, SaleModel saleModel) {
        //简单点,只让张三执行这个功能
        if(!"张三".equals(user)){
            System.out.println("对不起"+user+",你没有保存销售单的权限");
            //就不再调用被装饰对象的功能了
            return false;
        }else{
            return this.ebi.sale(user, customer, saleModel);
        }       
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
客户端
package cn.javass.dp.decorator.example6;

public class Client {
    public static void main(String[] args) {
        //得到业务接口,组合装饰器
        GoodsSaleEbi ebi = new LogDecorator(new CheckDecorator(new GoodsSaleEbo()));
        //准备测试数据
        SaleModel saleModel = new SaleModel();
        saleModel.setGoods("Moto手机");
        saleModel.setSaleNum(2);
        //调用业务功能
        ebi.sale("张三","张三丰", saleModel);
        ebi.sale("李四","张三丰", saleModel);
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

装饰模式的优缺点

  • 1:比继承更灵活
  • 2:更容易复用功能
  • 3:简化高层定义
  • 4:会产生很多细粒度对象

思考装饰模式

装饰模式的本质

装饰模式的本质是:动态组合

何时选用装饰模式

  • 1:如果需要在不影响其它对象的情况下,以动态、透明的方式给对象添加职责,可
    以使用装饰模式,这几乎就是装饰模式的主要功能
  • 2:如果不合适使用子类来进行扩展的时候,可以考虑使用装饰模式,因为装饰模式
    是使用的“对象组合”的方式。所谓不适合用子类扩展的方式,比如:扩展功能
    需要的子类太多,造成子类数目呈爆炸性增长。
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Famiglistimott

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值