目录
前言
设计模式的原理理解起来相对都不难,但是难的是对设计模式应用场景的把握。
为什么要学习设计模式呢?
对于优秀的源码、中间件看的似懂非懂,这或许对设计模式的理解度不够有很大的关系。通常来说优秀框架的源码一般类结构、类之间的关系极其复杂,各种类时常调来调去,所以,为了保证代码的扩展性、灵活性、可维护性等,代码中通常会使用到很多设计模式、设计原则或设计思想。因此如果对设计模式、设计思想等有一定的认识,可能会对阅读源码有很大程度的帮忙。
常说的23种设计模式一般分成创建型、结构型、行为型三类。
创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合或组装”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。
1. 装饰者模式概述
1.1 定义
动态地给一个对象添加一些额外的职责,而不需要改变原先类的结构。
若要扩展类的功能,装饰者模式比继承更具有弹性。装饰者模式(Decorator)也称为包装器(Wrapper)模式。
简单来说就是层层包装,从而达到增强功能的目的。
HeadFirst中形象的表示如下图:
类似的Java中的IO其实就是一个典型的装饰者模式
LineInputStream in = new LineInputStream(new BufferedInputStream(new FileInputStream("e://test.txt")));
如上述代码对读取的功能进行缓冲式读取、按行读取等层层包装,增强读文件的功能。这就是装饰模式的要旨。
1.2 应用场景
- 扩展一个类的功能。
- 动态增加功能,动态撤销。
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。
2. 咖啡店场景引入该设计模式
2.1 场景介绍
在购买咖啡的时候,可以要求加入各种调料(蒸奶、豆浆、摩卡...), 然后在计算价格的时候可以按加入的调料收取不同的费用。
2.2 应用继承的思想设计如下
直接应用继承设计出来的类是多么的复杂(根据数学的组合,比如4种咖啡,4种调料,每种咖啡只加一种调料就有4*4=16种组合了,如每种咖啡加两种调料那就是 4*4*4=64 种组合...),这真的就是类爆炸了,如下图:
2.3 用装饰器模式设计如何呢
如下图有四种咖啡、四种调料
- 1. 将四种咖啡设计成具体的组件类(继承于超类Beverage饮料类)
- 2. 四种调料设计成具体的装饰者(继承自超类CondimentDecorator调味料装饰类,CondimentDecorator超类又继承自Beverage超类)-- 这里可能有点难理解,其实简单来说只是为了能通过多态的方式实现互相调用,见下述代码理解可能会清晰一点。
当需要添加一种不同咖啡那么只需要线性增加一个咖啡类,或者是添加新配方(加不同调料组合给现成的咖啡)那么只需要动态的运用不同的调料装饰便可,从而实现灵活删除或增加成不同组合。
通过对比用继承模式实现会出现类爆炸,而用装饰器模式只需要A+B个类。 这也正是为什么说装饰器模式比继承更具有弹性呢。
2.4 咖啡店装饰模式样例代码如下
思路:
- 设计一个饮料超类(可为抽象类也可为接口类)
- 实际的咖啡实现该饮料超类接口
- 设计一个调味超类接口,该超类为了通过多态特性引用饮料类型需要继承自饮料超类(装饰器类和组件类继承同样的父类,这样我们可以对组件类“嵌套”多个装饰器类。)
- 实际的调味类继承自该调味超类
- 1. 组件Beverage 超类
public abstract class Beverage {
String description = "Unknown Beverage";
public abstract double cost();
public String getDescription(){
return description;
}
}
- 2. 具体的饮料类HouseBlend(注:当需要添加其他饮料类,只需按如下方式添加一个对应饮料类便可)
public class HouseBlend extends Beverage {
public HouseBlend(){
description = "HouseBlend Coffee, ";
}
@Override
public double cost() {
return 1.99;
}
}
- 3. 装饰者调料超类CondimentDecorator
public abstract class CondimentDecorator extends Beverage {
Beverage beverage;
public abstract String getDescription();
}
- 4. 具体的调料Milk类(注:当需要添加其他调料类,只需按如下方式添加一个对应调料类便可)
public class Milk extends CondimentDecorator {
public Milk(Beverage beverage){
this.beverage = beverage;
}
@Override
public double cost() {
return 0.1 + beverage.cost();
}
@Override
public String getDescription() {
return beverage.getDescription() + ",Milk ($:0.1)";
}
}
- 5. 咖啡店的测试类
public class CoffeeStore {
public static void main(String[] args) {
// 一杯纯HouseBlend咖啡
Beverage beverage = new HouseBlend();
System.out.println(beverage.description + ", $ "+beverage.cost());
// 双陪抹茶加牛奶的HouseBlend咖啡
Beverage beverage1 = new HouseBlend();
beverage1 = new Milk(beverage1);
beverage1 = new Mocha(beverage1);
beverage1 = new Mocha(beverage1);
System.out.println(beverage1.getDescription() + ", $ "+beverage1.cost());
// 换个类似IO方式的写法
Beverage beverage2 = new Mocha(new Mocha(new Milk(new HouseBlend())));
System.out.println(beverage2.getDescription() + ", $ "+beverage2.cost());
}
}
- 运行结果如下所示:
HouseBlend Coffee ($:1.99), $ 1.99
HouseBlend Coffee ($:1.99),Milk ($:0.1), Mocha ($:0.2), Mocha ($:0.2), $ 2.49
HouseBlend Coffee ($:1.99),Milk ($:0.1), Mocha ($:0.2), Mocha ($:0.2), $ 2.49
总结:用上述模式进行设计咖啡店类时,需要做的类个数是A+B,主要是运用了多态与继承以及组合等语法实现了该模式。
3. 以JDK IO源码来剖析装饰者模式
还记得当初第一次接触Java IO 类库感觉挺苦恼的,因为其非常庞大和复杂,有几十个类,负责 IO 数据的读取和写入。现对 Java IO 类做一下分类,从下面两个维度将它划分为四类。具体如下所示:
针对不同的读取和写入场景,Java IO 又在这四个父类基础之上,扩展出了很多子类。具体如下所示:
以下代码按行并支持缓存方式读取一个本地文件,这里的代码并没有用上缓存的特性只是以此为例而已
FileInputStream in = new FileInputStream("e://test.txt");
BufferedInputStream bufIn = new BufferedInputStream(in);
LineInputStream lineIn = new LineInputStream(bufIn);
String str;
while ((str = lineIn.readLine()) != null) {
System.out.println(str);
}
初学时感觉这么写还真是蛮麻烦的,为啥不直接弄一个类支持按行并支持缓存读取本地文件的类呢?按理来说这样的类实现起来很简单才对,假如有这么一个类LineBufFileInputStream那么要实现按行并支持缓存方式读取一个本地文件代码可为
LineBufFileInputStream in = new LineBufFileInputStream("e://test.txt");
- 注:上述代码看起来似乎实现起来更简单了,但实践上这有点类似咖啡店那个例子,当有很多种读取方式时必定会造成类爆炸。因此,还是按原JDK设计的方式运用类组合的方式实现装饰者模式来实现具体读取功能更具有弹性与可维护性。
- 在阅读JDK IO源码时,发现其设计跟咖啡类相当相似,例如InputStream类似于Beverage超类,读与写类似于饮料类,不同的读取方式类似于调料类,针对读与写的组件类可通过组合各种读取方式以增强读写功能。
模拟IO的设计,新添加一个将输入的内容转换成小写功能类
将输入的字节转换成小写,不扣细节哈,其实这个类的写法可以模拟现有的BufferedInputStream类的实现
- 简易代码如下:
public class LowerCaseInputStream extends FilterInputStream {
public LowerCaseInputStream(InputStream in) {
super(in);
}
public int read() throws IOException {
int c = in.read();
return (c == -1 ? c : Character.toLowerCase((char)c));
}
public int read(byte[] b, int offset, int len) throws IOException {
int result = in.read(b, offset, len);
for (int i = offset; i < offset+result; i++) {
b[i] = (byte)Character.toLowerCase((char)b[i]);
}
return result;
}
// 其他方法。。。
}
- 简易测试代码
public class InputTest {
public static void main(String[] args) throws IOException {
String file = "e:/test1.txt";
int c;
InputStream in = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream(file)));
while ((c = in.read()) >= 0) {
System.out.print((char) c);
}
in.close();
}
}
4. 模式的一些小感触
- 注:引自HeadFirst原文
总的来说:装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。
- 参考文档
极客时间《设计模式之美》、HeadFirst《设计模式》