从未这么明白的设计模式(三):装饰器模式

cover

本文原创地址:jsbintask的博客(食用效果最佳),转载请注明出处!

同系列文章:
从未这么明白的设计模式(二):观察者模式
从未这么明白的设计模式(一):单例模式

前言

装饰器模式是为了运行时动态的扩展一个类的功能。它谨遵开闭原则,它实现的关键在于继承和组合的结合使用,解耦对象之间的关系。
各种设计模式学习地址:https://github.com/jsbintask22/design-pattern-learning

栗子

首先我们列举一个案例,并且按照面向对象的思想来对应实体之间的关系。

有一个咖啡店,销售各种各样的咖啡,拿铁,卡布奇洛,蓝山咖啡等,在冲泡前,会询问顾客是否要加糖,加奶,加薄荷等。这样不同的咖啡配上不同的调料就会卖出不同的价格。


Decorator

V1

针对上面的栗子,我们很容易就抽象出对应的实现,如上图。接着,我们就要编写对应的类来实现对应的功能。在这个例子中,主题当然就是咖啡,并且它有一个属性是名字,一个行为 价格,出于“面向对象”的思想,我们自然会设计出抽象类Coffee:

Decorator

public abstract class Coffee {
    /**
     * 获取咖啡得名字
     */
    public abstract String getName();

    /**
     * 获取咖啡的价格
     */
    public abstract double getPrice();
}

接着,按照继承的思想,我们要开始设计出具体的实现类,因为拿铁,卡布奇洛,蓝山搭配上不同的调料(上面三种)会有不同的价格,名字,所以我们至少得设计出 3 X 3 = 9 个类来分别对应它们的名字和价格:

Decorator

嗯!我想不用说这样设计得缺陷也很明显了! 由于不同的咖啡和不同的调料得各种任意组合,使得出现了类爆炸的现象。既然有这么明显的缺陷,那我们当然得改! 我们可以考虑把各种调料当作属性加入到Coffee这个抽象类中,接着在实现类中计算价格和名字时,分别判断是否加入了各种调料包,得到不同的名字和价格!

按照上面的思想,我们的Coffee类现在变成了这样:

public abstract class Coffee {
    // 是否加了牛奶
    protected boolean addedMilk;
    // 是否加了糖
    protected boolean addedSugar;
    // 是否加了薄荷
    protected boolean addedMint;

    /**
     * 获取咖啡得名字
     */
    public abstract String getName();

    /**
     * 获取咖啡的价格
     */
    public abstract double getPrice();
}

接着,我们实现一种咖啡,蓝山咖啡:

public class BuleCoffee extends Coffee {
    @Override
    public String getName() {
        StringBuilder name = new StringBuilder();
        name.append("蓝山");
        if (addedMilk) {
            name.append("牛奶");
        }
        if (addedMilk) {
            name.append("薄荷");
        }
        if (addedSugar) {
            name.append("加糖");
        }
        return name.toString();
    }

    @Override
    public double getPrice() {
        double price = 10;
        if (addedMilk) {
            price += 1.1;
        }
        if (addedMilk) {
            price += 3.2;
        }
        if (addedSugar) {
            price += 2.7;
        }

        return price;
    }
}

嗯!现在似乎比上面愉快多了。其实不然!我们仔细分析这种设计,会发现它似乎不太符合”封装的思想“,比如说针对拿铁,对于加薄荷而言,对他总是多余的! 而对于蓝山而言,牛奶又显得很多余! 所以这种设计也并不合理。 另外,我们假设coffee,拿铁等实体类来自第三方类库,我们并不能改动这些类的实现, 又要怎么得到名字和价格呢?

这个时候,我们就得使用装饰器模式来动态的扩展类行为! 所以我们设计出V3版本。

V3

开闭原则

首先,我们需要了解一个面向对象的一个基本设计原则:开闭原则,它指的是类应该对修改关闭,对扩展开放

怎么理解呢? 就比如我们上方说的:假如cofee和它的一众实现拿铁,卡布奇洛,蓝山来自第三方类库,并且这个类库已经很”适合“,”实用“了。 而我们为了得到加入不同调料的咖啡的名字和价格,我们就得修改这些实现,而这样的修改,总是免不了稳定性的改变。对原本的系统来说也是一种风险! 所以我们应该 对修改关闭,对扩展开放;

继承和组合

遵循开闭原则,那我们就得对外扩展,那怎么对外扩展呢? 这也是装饰器模式实现的关键,利用继承和组合的结合; 现在我们可以考虑设计出一个装饰类,它也继承自coffee,并且它内部有一个coffee的实例对象:

Decorator

现在,我们多了一个咖啡装饰器: CoffeeDecorator:

public abstract class CoffeeDecorator implements Coffee {
    private Coffee delegate;

    public CoffeeDecorator(Coffee coffee) {
        this.delegate = coffee;
    }

    @Override
    public String getName() {
        return delegate.getName();
    }

    @Override
    public double getPrice() {
        return delegate.getPrice();
    }
}

接着,我们将牛奶,薄荷作为抽象出一个类,继承自CoffeeDecorator,所以,现在类图就成了这样:

Decorator

我们实现一个MilkCoffeeDecorator

public class MilkCoffeeDecorator extends CoffeeDecorator {
    public MilkCoffeeDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getName() {
        return "牛奶, " + super.getName();
    }

    @Override
    public double getPrice() {
        return 1.1 + super.getPrice();
    }
}

按同样的方法可以实现出MintCoffeeDecoratorSugarCoffeeDecorator。接着我们写一个测试类:

public class App {
    public static void main(String[] args) {
        // 得到一杯原始的蓝山咖啡
        Coffee blueCoffee = new BlueCoffee();
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());

        // 加入牛奶
        blueCoffee = new MilkCoffeeDecorator(blueCoffee);
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());

        // 再加入薄荷
        blueCoffee = new MintCoffeeDecorator(blueCoffee);
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());

        // 再加入糖
        blueCoffee = new SugarCoffeeDecorator(blueCoffee);
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());
    }
}
Decorator

从结果我们可以看出,随着不断加入各种调料,价格,名字都在改变! 这说明我们加入不同的调料,动态的改变了咖啡的名字和价格!

思考

从上面的最后的装饰器模式的实现来看,我们可以得出以下结论:

  1. 通过装饰器模式可以动态的将责任附加到原有的对象上,而不改变原有的code。
  2. 遵循开闭原则
  3. 装饰者和被装饰者有相同的父类(如栗子中的Coffee)
  4. 可以用多个装饰器装饰同一个对象。(见运行类)
  5. 装饰者可以在被装饰者的行为之前或之后动态的加上自己的行为。(参考装饰实现)
  6. 组合比继承更加的灵活(上面的coffee代理)

扩展

到现在,我们已经实现了一个自己的装饰器,我们来看看jdk中用到的装饰器实现.

IO

我们可以查看FilterInputStream:

Decorator

它的主要是实现者为BufferedInputStream:
Decorator

所以我们经常可以使用BufferedInputStream装饰一个InputStream,比如FileInputStream:
new BufferedInputStream(FileInputStream);
这就是装饰器模式的典型应用。

tomcat

在tomcat的HttpServletRequest的内部实现代码中,RequestFacde继承自HttpServlet,而它内部的实现也是通过代理Request对象,而Request对象继承自HttpServlet,Request内部代理了org.apache.coyote.Request来实现的。

总结

装饰器模式充分展示了组合的灵活。利用它来实现扩展。它同时也是开闭原则的体现。 如果相对某个类实现运行时功能动态的扩展。 这个时候你就可以考虑使用装饰者模式!

关注我,这里只有干货!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266