设计模式(四)—— 装饰者模式
目录
问题描述
版本(一)
版本(二)—— 装饰者模式
1. 版本(一)存在的缺点
2. 装饰者模式
3. 装饰者模式实现咖啡订单系统
装饰者模式的应用——java I/O
一个非常好的例子—— 编写自己的java I/O装饰者
问题描述
版本(一)
只使用继承
package HeadFirst.DecoratorPattern;
abstract public class Beverage {
protected String description;
//蒸奶
protected boolean milk = false; //是否要
protected int milkAmount = 0; //要几份
protected double milkCost = 2.0; //一份多少钱
//豆浆
protected boolean soy = false;
protected int soyAmount = 0;
protected double soyCost = 1.0;
//摩卡
protected boolean mocha = false;
protected int mochaAmount = 0;
protected double mochaCost = 3.0;
//奶泡
protected boolean whip = false;
protected int whipAmount = 0;
protected double whipCost = 2.0;
public String getDescription(){
return this.description;
}
public double cost(){ //计算所有小料花的价钱
double total = 0.0;
if(milk) total = total + this.milkAmount*this.milkCost;
if(soy) total = total + this.soyAmount*this.soyCost;
if(mocha) total = total + this.mochaAmount*this.mochaCost;
if(whip) total = total + this.whipAmount*this.whipCost;
return total;
}
//-----------运行时设置要与不要---------
public void setMilk(boolean milk) {
this.milk = milk;
}
public void setSoy(boolean Soy){
this.soy = soy;
}
public void setMocha(boolean mocha) {
this.mocha = mocha;
}
public void setWhip(boolean whip) {
this.whip = whip;
}
//------------运行时设置要几份---------
public void setMilkAmount(int milkAmount) {
this.milkAmount = milkAmount;
}
public void setSoyAmount(int soyAmount) {
this.soyAmount = soyAmount;
}
public void setMochaAmount(int mochaAmount) {
this.mochaAmount = mochaAmount;
}
public void setWhipAmount(int whipAmount) {
this.whipAmount = whipAmount;
}
}
以DarkRoast.java为例
package HeadFirst.DecoratorPattern;
public class DarkRoast extends Beverage{
private double cost = 12.0;
DarkRoast(){
description = "DarkRoast";
}
public double cost(){ //重写父类的cost方法
return super.cost() + this.cost;
}
}
客户端点单测试:
package HeadFirst.DecoratorPattern;
public class Client {
public static void main(String[] args) {
//来了一位顾客,点了一倍DarkRoast,加1份蒸奶,加2份摩卡
DarkRoast darkRoast = new DarkRoast();
darkRoast.setMilk(true);
darkRoast.setMilkAmount(1);
darkRoast.setMocha(true);
darkRoast.setMochaAmount(2);
System.out.println("您需要支付:"+darkRoast.cost()+"元");
}
}
运行结果:
您需要支付:20.0元
其实这样已经挺好的了哈哈哈。
- 如果要更改咖啡涨价或者小料涨价,我们只需要打开父类或者子类修改价钱就行了(尽管这样违背了开闭原则,但是开闭原则有时候该违背就要违背!)。
- 如果要增加新的小料奥利奥Oreo,只需要打开父类,加入新的成员变量Oreo、OreoAmount、OreoCost,加入新的成员方法setOreoAmount()和setOreo(),然后在cost方法中给Oreo再加一行就行了(当然,也违背了开闭原则)
- 如果要退出咖啡新品,更简单的了,直接增加一个咖啡子类就好啦,根本不违背开闭原则
非常好的问题:为什么Duck问题只使用继承不行,而咖啡点单问题勉强可以呢?
非常好的回答:和Duck问题不同,鸭子超类每增加一种行为(比如吃玉米粒),就要重新考虑所有子类是否具备这一种行为,然后所有子类都要重写这一行为。但是咖啡点单问题特殊在,是否具备某种小料,是顾客点单时才能确定的,程序员实现咖啡子类的时候是不需要去确定的,所以不存在重写的。
版本(二)—— 装饰者模式
1. 版本(一)存在的缺点
版本(一)的设计方式存在两个缺点,我认为第二点比较严重:
- 违背了开闭原则,这个前面已经详细解释过了
- 设计死板。
- 版本(一)规定了每种咖啡的类中包含了所有的小料,哪怕这种小料根本不适合加到这种咖啡中(永远用不到)。比如咖啡店出了一款新品“红茶”,那么“奥利奥碎”这种小料就永远用不到。
- 如果顾客只点了一杯DarkRoast,什么小料都不加,
开闭原则
软件的每个地方都采用开闭原则也没有必要,这会导致代码变得复杂且难以理解。
装饰者模式可以完美解决版本(一)存在的缺点。
2. 装饰者模式
定义:动态地将装饰品附加到被装饰对象上
装饰者模式完全遵循开闭原则
实现装饰者模式的关键在于:被装饰者和装饰者继承共同的父类,尽管这并不符合现实中的逻辑(奶茶和珍珠怎么能是同一父类呢)。
应用场景:
模板类图:
- 被装饰者和装饰者继承共同的父类
- 具体的装饰类把Component的引用作为成员变量
- 里面的超类可以是抽象类也可以是接口
- 装饰者可以在被装饰者行为之前/后加上自己的行为,甚至将被装饰者行为整个取代掉
装饰者模式的优缺点
缺点:
- 装饰者会导致设计过程中出现许多小对象,如果过度使用,程序会变得很复杂。new BBBBB( new BBBB ( new BBB ( new BB ( new B()))))
3. 装饰者模式实现咖啡订单系统
下面这个图更容易理解一些,“装饰者一层层包裹上去”
抽象类 Beverage.java
abstract public class Beverage {
protected String description = "Unknown";
protected double cost;
public abstract String getDescription();
public abstract double cost();
}
抽象类 CondimentDecorator.java
abstract public class CondimentDecorator extends Beverage{
}
DarkRoast.java
public class DarkRoast extends Beverage{
DarkRoast(){
description = "DarkRoast";
cost = 0.99;
}
@Override
public String getDescription() { //重写父类的getDescription方法
return this.description;
}
@Override
public double cost(){ //重写父类的cost方法
return this.cost;
}
}
Milk.java
public class Milk extends CondimentDecorator{
Beverage beverage; // !!!
Milk(Beverage beverage){ //实现Milk对咖啡的包裹
this.beverage = beverage;
this.cost = 0.1;//蒸奶0.1一份
}
@Override
public String getDescription() {
return beverage.getDescription()+",Milk";
}
@Override
public double cost() {
return beverage.cost() + this.cost;
}
}
Mocha.java
public class Mocha extends CondimentDecorator{
Beverage beverage;
Mocha(Beverage beverage){
this.beverage = beverage;
this.cost = 0.2;
}
@Override
public String getDescription() {
return beverage.getDescription()+",Mocha";
}
@Override
public double cost() {
return beverage.cost()+this.cost;
}
}
客户端测试:
public class Client {
public static void main(String[] args) {
Beverage darkRoast = new DarkRoast(); //一杯DarkRoast
darkRoast = new Milk(darkRoast); //加一份Milk
darkRoast = new Mocha(darkRoast); //加一份Mocha
System.out.println(darkRoast.getDescription());
System.out.println("您需要支付:"+darkRoast.cost()+"元");
}
}
输出:
DarkRoast,Milk,Mocha
您需要支付:1.29元
非常好的问题:有不变的咖啡,变化的小料,为什么不能封装变化(写一个小料interface,然后小料各自实现interface成为类,以组合/依赖的方式和咖啡类组合),使用策略模式呢?
非常好的回答:因为顾客不来的话,程序员根本不知道什么小料该和什么咖啡组合啊!和策略模式不同的是,装饰者模式适用于没有稳定对象的场景,即只有在运行时才知道对象是什么样子。就拿例题来说,如果一个顾客要DarkRoust里加1份摩卡,2份蒸奶,3份豆浆,4份奶泡;另一个顾客要HouseBlend加10份蒸奶和2份奶泡,需求成千上万,谁又能全部想到呢?就算穷举一遍,哪个系统能把它们都封装成类呢?所以符合顾客需求的类/对象只能在运行时生成,也就是在【前台点单】时生成,这就是装饰者模式和策略模式的最大区别。
非常好的问题:这种场景是不是用工厂模式、生成器模式、桥接模式也能实现
回答:有待学习
装饰者模式的应用——java I/O
以InputStream为例,看看javaI/O是如何利用装饰者模式的
其实,我们之前学习java I/O的时候,“处理流包裹在节点流上”就是装饰者去包裹被装饰者。
一个非常好的例子—— 编写自己的java I/O装饰者
装饰者类LowerCaseInputStream.java
import java.io.*;
public class LowerCaseInputStream extends FilterInputStream {
/**
* Creates a <code>FilterInputStream</code>
* by assigning the argument <code>in</code>
* to the field <code>this.in</code> so as
* to remember it for later use.
*
* @param in the underlying input stream, or <code>null</code> if
* this instance is to be created without an underlying stream.
*/
protected LowerCaseInputStream(InputStream in) { //实现LowerCaseInputStream对in的包裹
super(in);
}
@Override
public int read() throws IOException {
int c = super.read();
if(c == -1){
return -1;
}else{
return Character.toLowerCase((char)c); //变小写
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
//字节怎么变成字符???
return -1;
}
@Override
public int read(byte[] b) throws IOException {
//字节怎么变成字符???
return -1;
}
}
测试
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class Client {
public static void main(String[] args) {
LowerCaseInputStream lowerCaseInputStream = null; //装饰
try {
//1)
FileInputStream fileInputStream = new FileInputStream(new File("E:\\Java Project\\helloworld\\ioStream\\UpperCase.txt"));
lowerCaseInputStream = new LowerCaseInputStream(fileInputStream);
//2)读
int data;
while((data = lowerCaseInputStream.read())!=-1){
System.out.print((char)data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(lowerCaseInputStream != null)
lowerCaseInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
其中,我们的输入文件UpperCase.txt里的内容是:
STUPID TEACHER!
程序输出: