我同事大头,喜欢吃面。这天大头去一家面馆吃面,由于大头饭量大,面没吃完觉得不够,有点了两个卤蛋,一会儿再叫了一份青菜,最后时刻又加了一碗豆浆。面我们称为主食,必须要点,其他称为小吃,可以随意组合点单也可不点,假如面的价格是8元,卤蛋2元一个,青菜5元一份,豆浆3元一碗,现在要计算总共价格,如何设计?要求具备良好的扩展性和维护性。
1. 大头吃面第一版
既然面馆提供这么多好吃的东西,最容易的想到的就是在类中增加方法,初版设计类图如下:
这种方式,将点主食、小吃分别加到管理类中,然后计算总价,实现代码如下:
class NoodleRestaurant {
private int totalPrice;
void orderNoodles(int count) {
System.out.println("点了" + count + "份面");
totalPrice += 8 * count;
}
void addEggs(int count) {
System.out.println("点了" + count + "份鸡蛋");
totalPrice += 2 * count;
}
void addVegetables(int count) {
System.out.println("点了" + count + "份青菜");
totalPrice += 5 * count;
}
void addSoySauce(int count) {
System.out.println("点了" + count + "份豆浆");
totalPrice += 3 * count;
}
public int getTotalPrice() {
return totalPrice;
}
}
客户端调用也很简单:
NoodleRestaurant noodleRestaurant = new NoodleRestaurant();
noodleRestaurant.orderNoodles(1);
noodleRestaurant.addEggs(2);
noodleRestaurant.addVegetables(1);
noodleRestaurant.addSoySauce(1);
System.out.println("大头总共花费:" + noodleRestaurant.getTotalPrice() + "元");
这样的设计,虽然很简单容易想到,但是如果面馆现在添加了更多的小吃,怎么办呢?那么只能在NoodleRestaurant
类中继续增加方法来实现。很明显,这样就违背开闭原则,添加新功能的时候,原有代码也需要修改,并且,如果小吃和主食越来越多,类将变得非常庞大和臃肿。
改进的方式就是使用装饰模式。
2. 装饰模式
2.1. 装饰模式简介
装饰模式(Decorator Pattern)的定义:在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。装饰模式属于一种结构型模式,它比继承更灵活。
装饰模式的基本类图如下:
装饰模式分为以下几个角色:
Component: 具有特定功能的对象(构件),可以是抽象类或者接口,也称为被装饰者,需要对其添加职责
ConcreteComponent: 特定功能对象(Component)的具体实现
Decorator: 装饰抽象类,用来动态的给Component添加职责,扩展功能
ConcreteDecorator: 具体的装饰类实现
注意Decorator和Component的关系:
首先,Decorator继承或者实现了Component,这样就可以具备原Component对象的功能
其次,Decorator内部还聚合了Component,通常包含一个
setComponent(Component)
方法,也可以通过构建器聚合Component对象,这样做的目的:便于层层包装,被装饰过的类由于继承了Component,仍然可以聚合到Decorator从而被再次包装第三,对于Component而言,它无需知道Decorator的存在,只要客户端(Decorator的使用者)知道Decorator并使用它扩展的功能即可
装饰模式的特点:
比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
通过使用不同的装饰类及这些装饰类的排列组合,可以实现不同效果
完全遵守开闭原则
2.2. 装饰模式举例
举个简单的例子,装饰模式就好比商家给商品打包,商品有贵重、一般、廉价之分,有的商品一点磕碰都不行,比如手机、笔记本电脑等,它们都需要厚厚的包装才能避免损坏;而有的商品则不然,比如衣服,只要简单包装即可,因为他们不怕摔,没必要填充泡沫、充气袋等缓冲材料。
因此,商家会按照商品的贵重程度、耐压程序来决定采用什么材料打包,通常都是将商品外边添加一层层的打包用材料,比如泡沫、充气袋,然后再装入塑料袋或者纸盒。这样的模式其实就是装饰模式,商品就是Component,泡沫、充气袋等打包材料就是装饰器。快递打包设计类图如下:
我们抽象了一个物品的接口,它作为Component,下边有电脑、衣服两种商品作为ConcreteComponent,然后快递包装类作为Decorator,具体的打包材料作为ConcreteDecorator。再来看看示例代码:
// 物品接口
interface Item { (1)
void display();
}
// 电脑
class Computer implements Item { (2)
@Override
public void display() {
System.out.println("一台电脑");
}
}
// 衣服
class Clothes implements Item { (2)
@Override
public void display() {
System.out.println("一件衣服");
}
}
1 | Component角色,抽象类或者接口,定义功能规范 |
2 | ConcreteComponent角色,具体功能实现 |
// 快递包装,装饰者
abstract class GoodsDecorator implements Item { (1)
// 被装饰者
protected Item item;
public GoodsDecorator(Item item) { (2)
this.item = item;
}
public void setItem(Item item) { (2)
this.item = item;
}
@Override
public void display() {
this.item.display();
this.wrap();
}
// 包装快递
protected abstract void wrap(); (3)
}
1 | 装饰器继承了Item(Component角色) |
2 | 装饰器聚合了Item(Component角色) |
3 | 定义扩展方法,由子类实现 |
// 塑料袋
class PlasticBag extends GoodsDecorator {
public PlasticBag(Item item) {
super(item);
}
@Override
protected void wrap() { (1)
System.out.println("装入塑料袋");
}
}
// 泡沫材料
class FoamMaterial extends GoodsDecorator {
public FoamMaterial(Item item) {
super(item);
}
@Override
protected void wrap() { (1)
System.out.println("填充泡沫材料");
}
}
// 纸板箱
class Carton extends GoodsDecorator {
public Carton(Item item) {
super(item);
}
@Override
protected void wrap() { (1)
System.out.println("装入纸箱");
}
}
// 充气袋
class DunnageBag extends GoodsDecorator {
public DunnageBag(Item item) {
super(item);
}
@Override
protected void wrap() { (1)
System.out.println("填充充气袋");
}
}
1 | 子类(ConcreteDecorator角色)各自实现抽象装饰器定义的扩展方法 |
客户端调用也很简单:
System.out.println("商家打包一件衣服,不值钱,简单包装了事...");
Clothes clothes = new Clothes();
// 直接放入塑料袋就可以了
PlasticBag plasticBag = new PlasticBag(clothes);
plasticBag.display();
System.out.println("商家打包一台电脑,有点贵,还是做个良心商家好好包装一番...");
Computer computer = new Computer();
// 先用充气袋填充
DunnageBag dunnageBag = new DunnageBag(computer);
// 再用泡沫材料填充
FoamMaterial foamMaterial = new FoamMaterial(dunnageBag);
// 最后在装入纸箱,完成
Carton carton = new Carton(foamMaterial);
carton.display();
注意,客户端使用的是装饰器的display
方法,该方法才得到了扩展,而不是使用Component角色的方法。
结果输出如下:
商家打包一件衣服,不值钱,简单包装了事... 一件衣服 装入塑料袋 商家打包一台电脑,有点贵,还是做个良心商家好好包装一番... 一台电脑 填充充气袋 填充泡沫材料 装入纸箱
Ok,了解了装饰模式,我们看看如何改进文章开始提到"大头吃面"问题。
3. 大头吃面第二版
面馆主要是供应的是主食,比如面条、饺子等,而小吃作为附加食品给客人提供了更多的选择。使用装饰模式,可以将小吃看做对主食的一种包装,改进后的类图如下:
类图中,抽象出了一个接口:Food
,并且定义了cost()
方法来计算价格,实现类Noodle
支持设置点的面条的份数(count)。FoodDecorator
作为装饰器,继承并聚合了Food
,其子类都支持设置点单份数(count)。
改进后的代码如下:
// 抽象构件:食物
interface Food {
// 计算食物的价格
int cost(); (1)
}
// 主食:面条
class Noodle implements Food {
private int count = 1;
public Noodle() {
System.out.println("点了1份面");
}
public Noodle(int count) {
System.out.println("点了" + count + "份面");
this.count = count;
}
// 价格
@Override
public int cost() { (2)
return 8 * this.count;
}
}
1 | 定义功能方法,计算价格 |
2 | 功能方法的具体实现 |
abstract class FoodDecorator implements Food {
protected Food food;
public FoodDecorator(Food food) {
this.food = food;
}
@Override
public int cost() { (1)
return food.cost();
}
}
// 卤蛋
class Egg extends FoodDecorator {
private int count = 1;
public Egg(Food food) {
super(food);
System.out.println("点了" + count + "份鸡蛋");
}
public Egg(Food food, int count) {
super(food);
System.out.println("点了" + count + "份鸡蛋");
this.count = count;
}
@Override
public int cost() { (2)
return super.cost() + 2 * this.count;
}
}
// 青菜
class Vegetable extends FoodDecorator {
private int count = 1;
public Vegetable(Food food) {
super(food);
System.out.println("点了" + count + "份青菜");
}
public Vegetable(Food food, int count) {
super(food);
System.out.println("点了" + count + "份青菜");
this.count = count;
}
@Override
public int cost() { (2)
return super.cost() + 5 * this.count;
}
}
// 豆浆
class SoySauce extends FoodDecorator {
private int count = 1;
public SoySauce(Food food) {
super(food);
System.out.println("点了" + count + "份豆浆");
}
public SoySauce(Food food, int count) {
super(food);
System.out.println("点了" + count + "份豆浆");
this.count = count;
}
@Override
public int cost() { (2)
return super.cost() + 3 * count;
}
}
1 | 这里装饰器没有定义新的方式,而是重写了cost方法来计算总价 |
2 | 子类重写父类的cost方法,实现商品总价的计算 |
客户端调用:
System.out.println("第二版:");
// 点了一碗面
Noodle noodle = new Noodle();
// 加两个鸡蛋
Egg egg = new Egg(noodle, 2);
// 加一份青菜
Vegetable vegetable = new Vegetable(egg);
// 加一份豆浆
SoySauce soySauce = new SoySauce(vegetable);
// 计算价格
int cost = soySauce.cost();
System.out.println("大头总共花费:" + cost + "元");
程序输出结果如下:
第二版: 点了1份面 点了2份鸡蛋 点了1份青菜 点了1份豆浆 大头总共花费:20元
如果现在面馆要新增加小吃或者主食,只需要添加新的类实现Food
接口,或者继承FoodDecorator
接口供客户端调用即可,原来设计的代码不需要做任何改动,符合开闭原则。
4. 装饰模式在JDK中的应用
装饰模式在JDK中使用比较广泛,尤其是在IO操作中,比如BufferedReader
、DataInputStream
等等,看一个例子:
BufferedReader reader = new BufferedReader(new FileReader(DecoratorInJdkDemo.class.getResource("test").getFile()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
示例展示用了从test文件逐行读取文本信息,这是这个BufferedReader
就使用了装饰模式。
类的继承关系如下:
BufferedReader
从字符流中读取字符,并且提供缓冲区读取部分数据到内存处理,提高读取效率InputStreamReader
用来读取字节流FileReader
用于读取字符文件中的内容,它继承自InputStreamReader
public class BufferedReader extends Reader {
private Reader in;
// ......
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
// ......
}
Reader
是一个抽象类, 定义read抽象方法,BufferedReader
继承Reader
,并且聚合了Reader
,这里的BufferedReader既作为抽象装饰器又作为装饰器实现,扩展了Reader的功能,添加了缓冲功能,而FileReader作为Reader的具体实现,详情可以参考JDK源代码。