zoukankan      html  css  js  c++  java
  • 设计模式之装饰者模式(Decorator Pattern)

    一.什么是装饰者模式?

    装饰者模式能够完美实现“对修改关闭,对扩展开放”的原则,也就是说我们可以在不修改被装饰者的前提下,扩展被装饰者的功能。

    再来看看我们的文件操作代码:

    1
    InputStream in = new BufferedInputStream(new FileInputStream(file));

    被“包裹”在最内层的InputStream对象是new FileInputStream(file),是基本的文件输入流,用BufferedInputStream对象来扩展它的功能,甚至我们还可以这样:

    P.S.注意上面用到的动词——“包裹”,这就是装饰者模式的核心了

    1
    InputStream in = new LineNumberInputStream(new BufferedInputStream(new FileInputStream(file)));

    继续添加一层新的装饰LineNumberInputStream,以扩展获取行号的功能,当然,我们还可以扩展更多的功能,只要继续添加新的装饰就好了

    回过头来想想,我们给FileInputStream添加了一层层装饰,获得了一个个功能,在此过程中,我们实现了功能的动态扩展,但并没有修改被装饰者FileInputStream的任何东西。

    这就是所谓的“对修改关闭,对扩展开放”原则。

    二.举个例子

    假设我们要开店卖Milk,可选的配料有摩卡Mocha(巧克力味),咖啡Coffee,冰水IceWater,当然,如果卖得好的话我们还打算引进新的饮料(Orange、Yoghurt等等)以及新的配料(Salt。。玩笑)

    Milk本身有价格,并且会在节假日打折,各种不同的配料价格也不同,当然,我也可以点一杯加双份IceWater的Milk。。

    -------

    最容易想到的解决方案是:

    定义一个Milk类,包含很多属性,例如hasMocha, hasCoffee, hasIceWater(用来表示已添加的配料),还需要discount属性(用来表示折扣信息)、cost属性(用来表示价格)

    这就好了吗?不,除此之外我们还需要MochaNum, CoffeeNum, IceWaterNum(用来表示配料的份数,重口味顾客需要双份或者更多的配料。。)

    问题解决了,可是这样做真的好吗?

    考虑以下这些情况:

    1.引进一种新的饮料Orange(我们需要定义一个Orange,几乎没有复用的部分,从零开始。。或者,我们可以定义一个Beverage基类,把饮料共有的部分放进去)

    2.引进一种新的配料Salt(我们必须修改Milk类,添加hasSalt, SaltNum属性以满足加盐Milk需求。。)

    。。。

    现在看来我们的解决方案很差,不能适应任何变化,要扩展功能就可能必须修改已有的封装好的代码,而且还存在一个性能上的问题:计算饮料价格部分需要大量的if...else...结构,使得我们的代码很臃肿,且难以复用(不同饮料配料可能不同,计算价格的方法也不同)

    -------

    是时候尝试装饰者模式了

    首先,因为被装饰者与装饰者必须要具有相同的超类(暂不解释为什么),所以,我们定义下面的Beverage基类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package DecoratorPattern;
     
    /**
     * @author ayqy
     * 定义Beverage超类,所有具体Beverage和Ingredient都必须扩展自此类
     *
     */
    public abstract class Beverage {
        String desc = "Unknown Beverage";//定义饮料相关描述信息
        float cost;//定义饮料的价格
         
        public abstract float getCost();//定义cost方法返回该饮料的价格,子类必须实现此方法
         
        public String getDesc(){
            return desc;
        }
    }

    有了Beverage就可以开始定义被装饰者——Milk:

    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
    package DecoratorPattern;
     
    /**
     * @author ayqy
     * 定义具体Beverage:Milk类
     *
     */
    public class Milk extends Beverage{
         
        float discount = 1;//定义折扣,节假日Milk可能会打折(默认不打折)
         
        public float getDiscount() {
            return discount;
        }
     
        public void setDiscount(float discount) {
            this.discount = discount;
        }
     
        public Milk(){
            cost = 4.5f;//初始化Milk的价格
            desc = "Milk";//初始化描述信息
        }
     
        @Override
        public float getCost(){
            return discount * cost;//返回打折后的价格
        }
    }

    接下来是装饰者,因为装饰者具有一些不同于Beverage的特性,所以我们对其进行抽象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package DecoratorPattern;
     
    /**
     * @author ayqy
     * 定义Ingredient佐料类,继承自Beverage(在装饰者模式中,装饰者与被装饰者必须具有相同的超类)
     *
     */
    public abstract class Ingredient extends Beverage{
         
        Beverage beverage;//需要添加该佐料的饮料
     
        @Override
        public String getDesc() {
            return "(" + desc + ")" + beverage.getDesc();//佐料的描述应当带上括号,以区别佐料与饮料
        }
     
        @Override
        public float getCost() {
            return cost + beverage.getCost();//配料没有折扣,直接返回其价格 + 饮料价格
        }
         
        //在此添加其它Ingredient不同于Beverage的属性与行为
    }

    注意上面的getDesc与getCost方法,我们把计算价格与生成描述信息的责任完全委托给方法调用机制了,以至于代码是如此的简洁。。

    下面定义具体配料——IceWater,Coffee,Mocha:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package DecoratorPattern;
     
    /**
     * @author ayqy
     * 定义配料IceWater冰水
     *
     */
    public class IceWater extends Ingredient{
         
        public IceWater(Beverage bev)
        {
            cost = 0.5f;
            desc = "IceWater";
            beverage = bev;
        }
    }

    一切准备就绪,我们的Milk小店可以开张了。。

    三.效果示例

    先定义一个测试类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package DecoratorPattern;
     
    public class Test {
        public static void main(String[] args){
            Beverage bev;
             
            System.out.println("做一杯加摩卡加咖啡的Milk。。");
            bev = new Milk();//先做一杯Milk
            bev = new Mocha(bev);//添加Mocha
            bev = new Coffee(bev);//添加Coffee
            System.out.println(bev.getDesc() + " " + bev.getCost() + "¥");
             
            System.out.println("做一杯加双份冰水双份摩卡的咖啡Milk。。");
            bev = new Milk();//重新做一杯Milk
            bev = new IceWater(bev);//添加冰水
            bev = new IceWater(bev);//添加冰水
            bev = new Mocha(bev);//添加Mocha
            bev = new Mocha(bev);//添加Mocha
            bev = new Coffee(bev);//添加Coffee
            System.out.println(bev.getDesc() + " " + bev.getCost() + "¥");
            //当然也可以这样写: Beverage bev = new Coffee(new Mocha(new Milk()));
            //对比我们熟悉的方法链: InputStream in = new BufferedInputStream(new FileInputStream(file));
        }
    }

    结果示例:

    -------

    效果不错吧,再考虑之前的扩展问题:

    1.引进一种新的饮料Orange(我们需要定义一个Orange类继承自Beverage基类,可以复用Beverage基类中已有的部分,如果还不满意,当然也可以抽象出一个“具体饮料类ConcreteBeverage”,让Milk等其它饮料在此基础上扩展)

    2.引进一种新的配料Salt(我们不必对Milk类做任何修改,只需要实现一种Salt配料,继承自Ingredient类就好了)

    。。。

    发现装饰者模式的优点了吗?

    那么是时候泼一盆冷水了。。

    四.装饰者模式的优缺点

    缺点其实显而易见——你见过这么长的代码吗?

    1
    XObject o = new XDecorator(new XXDecorator(new XXXDecorator(new XXXXDecorator())));

    嗯,它只是给被装饰对象做了三次功能扩展而已,当然,还可以更多。。也就意味着可以更长

    而且,我们在使用时创建了很多小对象,就像这样:

    1
    2
    3
    4
    5
    6
    bev = new Milk();//重新做一杯Milk
    bev = new IceWater(bev);//添加冰水
    bev = new IceWater(bev);//添加冰水
    bev = new Mocha(bev);//添加Mocha
    bev = new Mocha(bev);//添加Mocha
    bev = new Coffee(bev);//添加Coffee

    让一个不熟悉装饰者模式的人来读上面的代码,他能很快弄明白吗?

    注意,上面的代码就解释了开篇提到的动词——“包裹”,对吗?

    -------

    优点:

    除了上面提到的动态扩展优点,还有一个更重要的优点就是前面提到的getDesc与getCost方法

    没错,我们可以利用这种调用机制来完成我们的操作(在装饰动作前或者装饰动作后添加我们的自定义操作就好了,例子里其实属于在装饰动作后添加操作),我们很轻易的达到了类似于递归的效果

    这也就解释了“为什么装饰者与被装饰者要具有相同的超类?”,还需要更多一点的解释:

    有一种设计原则是“多用组合,少用继承”,这里我们好像违背了这个原则吧

    其实并没有违背原则,装饰者模式中的继承是为了获得类型的匹配,而不是为了利用继承来扩展类的行为,而“多用组合,少用继承”原则省略掉的前提条件是“(当我们需要扩展类的行为时)多用组合,少用继承”

    <原创>黯羽轻扬 欢迎转载 不必注明原文出处</原创>
    <声明>作者水平有限 错误在所难免 欢迎指正</声明>
    <邮箱>835412398@qq.com 交流方可进步</邮箱>
  • 相关阅读:
    Git忽略规则.gitignore梳理
    makefile 系统脚本分析
    Kipmi0 占用CPU 100%
    kubectl检查组件健康状态异常处理
    Java 数组如何转成List集合
    用户控件中多控件自定义单击事件
    DevExpress启动窗体SplashScreen
    使用C#获取统计局行政区划代码
    解决mysqlclient模块安装问题
    com.aspose.words 类LoadOptions
  • 原文地址:https://www.cnblogs.com/jiligalaer/p/3963816.html
Copyright © 2011-2022 走看看