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

cover

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

前言

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

栗子

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

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

V1

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

1
2
3
4
5
6
7
8
9
10
11
public abstract class Coffee {
/**
* 获取咖啡得名字
*/
public abstract String getName();

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

接着,按照继承的思想,我们要开始设计出具体的实现类,因为拿铁,卡布奇洛,蓝山搭配上不同的调料(上面三种)会有不同的价格,名字,所以我们至少得设计出 3 X 3 = 9 个类来分别对应它们的名字和价格:
Decorator
嗯!我想不用说这样设计得缺陷也很明显了! 由于不同的咖啡和不同的调料得各种任意组合,使得出现了类爆炸的现象。既然有这么明显的缺陷,那我们当然得改! 我们可以考虑把各种调料当作属性加入到Coffee这个抽象类中,接着在实现类中计算价格和名字时,分别判断是否加入了各种调料包,得到不同的名字和价格!

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Coffee {
// 是否加了牛奶
protected boolean addedMilk;
// 是否加了糖
protected boolean addedSugar;
// 是否加了薄荷
protected boolean addedMint;

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

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

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

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
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。接着我们写一个测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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来实现的。

总结

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

关注我,这里只有干货!

×

谢谢你支持我分享知识

扫码支持
扫码打赏,心意已收

打开微信扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 前言
  2. 2. 栗子
  3. 3. V1
  4. 4. V3
    1. 4.1. 开闭原则
    2. 4.2. 继承和组合
    3. 4.3. 思考
  5. 5. 扩展
    1. 5.1. IO
    2. 5.2. tomcat
  6. 6. 总结
欢迎扫描左方二维码跟作者交流.