人类所有的力量,只是耐心加上时间的混合,所谓强者,是既有意志,又能等待时机。——巴尔扎克《人间喜剧》
欢迎来到星巴磁咖啡
星巴兹是以扩展速度最快而闻名的咖啡连锁店。如果你在街角看到它,在对面街上还会看到另一家。因为扩张速度太快了,他们准备更新订单系统,以满足他们的饮料供应要求。
他们原先的设计是这样的...
![](https://img.haomeiwen.com/i9064767/17d345fb3ae323da.png)
不熟悉咖啡的朋友不知道这个类是啥,我们做个简单的翻译
星巴克的House Blend怎么喝啊?
是北美特别流行的黑咖啡,它属于混合咖啡。但主要成分都有50%以上的哥伦比亚咖啡豆,50%以上都是咖啡豆.星巴克有售,可以翻译为家常咖啡或者首选咖啡,口感顺滑/清爽的口味
Dark Roast 深度烘培浓缩咖啡,一杯你最喜欢的深焙咖啡很容易让你迷失。这些咖啡的特色是强劲的口味和丰富的口感。每一杯都回味无穷,直到最后一滴。
Decaf 最大的卖点就是99%不含咖啡因的星巴克拿铁咖啡,就是几乎没有咖啡豆。
Espresso 特浓咖啡,口感浓重,浓咖啡!
奶泡是牛奶经过搅打后形成的一层泡沫
顾客跑到店里买咖啡了,想加点豆浆(Soy),摩卡(Mocha,也就是巧克力风味),热奶(Steamed Milk)
,星巴兹会根据所加入的调料收取不同的费用。所以订单系统必须考虑调料部分!
你也来尝试一下吧!
我们先从Beverage 基类下手,加上实列变量代表是否加上调料(condiment)(牛奶,豆浆,摩卡,奶泡。。。。。。)
![](https://img.haomeiwen.com/i9064767/d6128705e38986a6.png)
![](https://img.haomeiwen.com/i9064767/630d2dff740e207a.png)
先试着把代码补充完整
package head.firest.decorator;
public class Beverage {
protected String description;
public double cost() {
}
}
package head.firest.decorator;
public class DarkRoast extends Beverage {
public DarkRoast() {
description = "Excellment Dark Roast";
}
public double cost() {
}
}
小伙伴,尝试着把他们补充完整。
![](https://img.haomeiwen.com/i9064767/b47d19066a45ce15.png)
package head.firest.decorator;
public class Beverage {
protected String description;
protected boolean milk;
protected boolean soy;
protected boolean mocha;
protected boolean whip;
protected float milkCost;
protected float soyCost;
protected float mochaCost;
protected float whipCost;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isMilk() {
return milk;
}
public void setMilk(boolean milk) {
this.milk = milk;
}
public boolean isSoy() {
return soy;
}
public void setSoy(boolean soy) {
this.soy = soy;
}
public boolean isMocha() {
return mocha;
}
public void setMocha(boolean mocha) {
this.mocha = mocha;
}
public boolean isWhip() {
return whip;
}
public void setWhip(boolean whip) {
this.whip = whip;
}
public double cost() {
double condimentCost = 0;
if (isMilk()) {
condimentCost += milkCost;
}
if (isSoy()) {
condimentCost += soyCost;
}
if (isMocha()) {
condimentCost += mochaCost;
}
if (isWhip()) {
condimentCost += whipCost;
}
return condimentCost;
}
}
package head.firest.decorator;
public class DarkRoast extends Beverage {
public DarkRoast() {
description = "Excellment Dark Roast";
}
public double cost() {
return super.cost()+ 1.99f;
}
}
看吧!一共只需要五个类,通过思考将来可能需要的变化,我可以看出这种方法有一些潜在的问题。
当哪些需求或因素改变时会影响这个设计?
- 一旦出现新的调料,我们就需要加上新的方法,并改变超类中的cost()方法。
- 调料价钱的改变会使我们更改现有代码
- 以后可能会开发出新的饮料。对这些饮料而言(例如:冰茶) ,某些调料可能并不适合,但是在这个设计方式中,Tea(茶)子类将继承哪些不合适的方法,例如:hasWhip()(加奶泡)
万一顾客想要双倍摩卡咖啡,怎么办?这是不是增加一个功能很麻烦呢!这个设计不太好,接下俩我们来看看大师和门徒的对话。
大师:对于继承的冥想,可有精进?
门徒:是的,大师。尽管继承威力大,但是我体会到它并不总是能够实现最有弹性和最好维护的设计。
大师:啊!是的,看来你已经有所长进。那么,告诉我,我的门徒,不通过继承如何达到复用?
门徒:大师,我已经了解到利用组合和委托可以在运行时具有继承行为的效果。
大师:好,好,继续。。。。。。
门徒:利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相应的行为。然后,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态的扩展。
大师:很好,你已经开始看到组合的威力了。
门徒:是的,我可以利用此技巧把多个新职责,甚至是设计超类时还没有想到的职责加在对象上。而且,可以不用修改原来的代码。
大师:利用组合维护代码,你觉得效果如何?
门徒:通过动态组合对象,可以写新的代码添加新的功能,而无需修改现有代码。既然不改变现有代码,那么产生bug的机会将会大大减少。
大师:非常好,今天的谈话就到这里。希望你能在这个主题上更深入,干的不错。
开放 关闭原则
设计原则:类应对扩展开放,对修改关闭。
我们的目标时允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。实现这样的目标,有什么好处呢?这样的设计具有弹性可以应对改变,可以接受新的功能来应对改变的需求。
对扩展开放,对修改关闭?听起来很矛盾。设计如何兼顾两者?
答:这是一个很好的问题。乍听之下,的确感到矛盾,毕竟,越难修改的事务,就越难以扩展,不是吗?
但是,有一些聪明的OO技巧,允许系统在不修改代码的情况下,进行功能扩展。想想上一篇博客中的观察者模式,通过加入新的观察者,我们可以在任何时候扩展主题,而且不需要向主题中添加代码。以后,你还会陆续看到更多的扩展行为的其他OO设计技巧。
如何让设计的部分都遵循开放-关闭原则?
答:通常,你办不到。要让OO设计同时具备开放性和关闭性,又不修改现有的代码,需要花费许多时间和努力。一般来说,我们实现没有闲工夫把设计的每个部分都这么搞。你需要把注意力放到设计中最有可能改变的部分,让后应用开放-关闭原则。
认识装饰者模式
我们已经了解了继承无法完全解决问题,之前的设计中很大的问题时基类加入的新功能并不适用于所有的子类,而且功能扩展受限制。
所以,我们采用不一样的做法:我们要以饮料为主体,然后在运行时以调料装饰饮料。比方说,如果顾客想要摩卡和奶泡深度烘培浓缩咖啡,那么,要做的是
- 拿一个深度烘培浓缩咖啡(Dark Roast)对象
- 以摩卡(Mocha)对象装饰它
- 以奶泡(Whip)对象装饰它
- 调用cost()方法,并依赖委托(delegate)将调料的价钱加上去
好了!但是如何装饰一个对象,而 “委托”又要如何与此搭配使用呢?给一个暗示:把装饰着对象当成包装者,让我们看看这是如何工作的
以装饰着构造饮料订单
![](https://img.haomeiwen.com/i9064767/b03a0c457eeee41b.png)
![](https://img.haomeiwen.com/i9064767/ed4f2108e923548c.png)
好了,这是目前所知道的一切。。。。。。
- 装饰者和被装饰者对象有相同的类型
- 你可以用一个或者多个装饰着包装一个对象
- 装饰者可以在被装饰者行为前/后,加上自己的行为,以达到特定的目的。
- 对象可以在任何时候被装饰,所以可以在运行时动态地,不限量地用你喜欢的装饰者来装饰对象
装饰者模式 动态q将责任附件到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
装饰我们的饮料,让星巴兹的饮料也能符合此框架
![](https://img.haomeiwen.com/i9064767/6b90463ef1fbd584.png)
在往下之前,想想如何实现咖啡和调料的cost()方法。也思一下如何实现调料的getDescription()方法。
办公室对话 在继承和组合之间观念有一些混淆
哎呀,原以为在这个模式中,不会使用继承,而是要利用组合取代继承。
Sue:这话怎么说?
Jane:看看类图。CondimentDecorator 扩展自Beverage类,这用到了继承,不是吗?
Sue:的确如此,但我认为,这么做的重点在于,装饰者和被装饰者必须是一样的类型,也就是有共同的超类。在这里,我们用继承达到类型匹配,而不是利用继承获得行为。
Jane:行为来自装饰着和基础组件,或与其它装饰者之间的组合关系。
Sue:是的。如果依赖继承,那么类的行为只能在编译时静态决定。换句话说,如果行为不是来自超类,就是子类覆盖后的版本。反之,利用组合,可以把装饰着混和着用
Jane:我们可以在任何时候,实现新的装饰者增加新的行为。如果依赖继承,每当需要行为,还得修改现有的代码。
新咖啡师傅特训
店里走进一名顾客,要一杯 双倍摩卡豆浆奶泡拿铁咖啡,画一个图表达你的设计,采用和几页前一样的格式
这张图是 深度烘培浓缩咖啡摩卡奶泡
![](https://img.haomeiwen.com/i9064767/6481a59b622b60f8.png)
星巴兹咖啡
浓缩 ¥1.99
低咖啡因 ¥1.05
深焙 ¥0.99
综合 ¥0.89
配料
牛奶 ¥0.10
摩卡 ¥0.20
豆浆 ¥0.15
奶泡 ¥0.10
![](https://img.haomeiwen.com/i9064767/07de78fc146e066f.png)
写下星巴兹的代码
该时把设计变成真正代码的时候了!先从Beverage类下手,这不需要改变星巴兹原始设计。如下所示:
package head.firest.decorator2;
/**
* 饮料抽象类
*/
public abstract class Beverage {
String description = "Unkow Beverage";
public String getDescription() {
return description;
}
public abstract double cost();
}
package head.firest.decorator2;
/**
* 调料 抽象装饰者
*/
public abstract class CondimentDecorator extends Beverage {
// 所有的调料装饰者都必须重新实现getDescription方法,稍后我们会解释为什么
public abstract String getDescription();
}
写饮料的代码
现在,已经有了基类,让我们开始实现饮料把!先从浓缩咖啡开始,我们需要为具体的饮料设置描述,而且还必须实现cost()方法
package head.firest.decorator2;
/**
* 浓缩咖啡 ==> 具体组件 <==>被装饰者
*/
public class Espresso extends Beverage {
//为了设置饮料的描述,我们写了一个构造器
public Espresso() {
description = "Espresso";
}
//计算Espresso 饮料的价钱,现在不需要管调料的价钱
//直接把Espresso 的价钱返回1.99即可
@Override
public double cost() {
return 1.99;
}
}
package head.firest.decorator2;
/**
* 首选咖啡 ==> 具体组件 <==>被装饰者
*/
public class HouseBlend extends Beverage {
public HouseBlend() {
description = "House Blend";
}
// 计算House Blend 饮料的价钱,现在不需要管调料的价钱
// 直接把House Blend 的价钱返回0.89即可
@Override
public double cost() {
return 0.89;
}
}
写调料代码
如果你回头看看装饰着模式的类图,将发现我们已经完成了抽象组件(Beverage),有了具体组件(HouseBlend),也有了抽象装饰着(CondimentDecorator),现在,我们就来实现具体装饰者。先从魔卡下手:
package head.firest.decorator2;
/**
* 摩卡调料装饰者
*/
public class Mocha extends CondimentDecorator {
private Beverage mBeverage;
/**
* 用一个实例变量记录饮料,也就是被装饰者
* @param beverage
*/
public Mocha(Beverage beverage) {
this.mBeverage = beverage;
}
/**
* 饮料 + 调料的详细描述
*/
@Override
public String getDescription() {
return mBeverage.getDescription() + ",Mocha";
}
@Override
public double cost() {
return 0.20 + mBeverage.cost();
}
}
小伙伴们,写下Soy 和 Whip 调料的代码。我们接下来会实例化一个饮料对象,然后用各种调料包装它。
是时候舒服的坐下来,点一些咖啡,看看你利用装饰者模式设计出的灵活系统是多么神奇了。这是用来下订单的一些测试代码
package head.firest.decorator2;
public class StarbuzzCoffee {
public static void main(String[] args) {
Beverage beverage = new Espresso(); // 订一杯Espresso,不需要调料,打印出它的价格
System.out.println(beverage.getDescription() + " ¥" + beverage.cost());
Beverage beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2);// 用Mocha装饰DarkRoast
System.out.println(beverage2.getDescription() + " ¥" + beverage2.cost());
beverage2 = new Mocha(beverage2); // 用第二个Mocha装饰它
beverage2 = new Whip(beverage2); // 用奶泡装饰它
System.out.println(beverage2.getDescription() + " ¥" + beverage2.cost());
Beverage beverage3 = new HouseBlend(); //最后再来一杯调料为豆浆 摩卡 奶泡的HouseBlend咖啡
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription() + " ¥" + beverage3.cost());
}
}
运行结果:
Espresso ¥1.99
Dark Roast,Mocha ¥1.19
Dark Roast,Mocha,Mocha,Whip ¥1.49
House Blend,Soy,Mocha,Whip ¥1.34
在菜单上加上咖啡的容量大小,供顾客选择小杯(tall),中杯(grande),大杯(venti)。星巴兹认为这是任何咖啡都是必须具备的,所以在Beverage 类中加上了getSize()与setSize()。他们也希望调料根据咖啡容量收费,例如:小中大杯的咖啡加上豆浆,分别加收0.10 0.15 0.20 元。
如何改变装饰者类应对这样的需求?
![](https://img.haomeiwen.com/i9064767/724a290e41f22f07.png)
package head.firest.decorator2;
public class Soy extends CondimentDecorator {
private Beverage mBeverage;
public Soy(Beverage beverage) {
this.mBeverage = beverage;
}
@Override
public String getDescription() {
return mBeverage.getDescription() + ",Soy";
}
//小杯 中杯 大杯 的咖啡加上豆浆,分别加收0.10 0.15 0.20元
@Override
public double cost() {
double cost = mBeverage.cost();
if (mBeverage.getSize() == Beverage.TALL) {
cost += 0.10;
} else if (mBeverage.getSize() == Beverage.GRANDE) {
cost += 0.15;
} else if (mBeverage.getSize() == Beverage.VENTI) {
cost += 0.20;
}
return cost;
}
}
真实世界的装饰者
java.io 包内的类太多了,简直是排山倒海。你第一次看到这些API发出哇的惊叹时,放心,你不是唯一收到惊吓的人。这些I/O相关的类对你来说更有意义,因为其中许多类都是装饰者。下面是一个典型的对象集合,用装饰者来将功能结合起来,以读取文件数据:
![](https://img.haomeiwen.com/i9064767/4fb4599c07fff7b7.png)
我们发现,和星巴兹的设计相比,java.io其实没有多大的差异。
但是java.io 也引出装饰者模式的一个缺点,利用装饰着模式,常常造成设计中有大量的小类,数量真多,可能会造成使用此API程序员的困扰。但是,现在你已经了解了装饰者的工作原理,以后使用别人大量装饰API,就可以很容易辨别出他们的装饰者类是如何组织的,以方便包装方式取得想要的行为。
编写自己的java.io装饰者
你已经知道装饰者模式,也看过java I/O类图,应该已经准备好编写自己的输入装饰着。编写一个装饰着,把输入流内的所有大写字母转换成小写字母。
package head.first.decorator2;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 扩展自FilterInputStream,这是所有InputStream 的抽象装饰者
*/
public class LowerCaseInputStream extends FilterInputStream {
protected LowerCaseInputStream(InputStream in) {
super(in);
}
/**
* 一个针对字节,把大写字符转换成小写字符
*/
@Override
public int read() throws IOException {
int c = super.read();
return c == -1 ? c : Character.toLowerCase(c);
}
/**
* 一个针对字节数组,把大写字符转换成小写字符
*/
@Override
public int read(byte[] b, int off, int len) throws IOException {
int result = super.read(b, off, len);
for (int i = off; i < off + result; i++) {
b[i] = (byte) Character.toLowerCase(b[i]);
}
return result;
}
}
package head.first.decorator2;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class TestInput {
public static void main(String[] args) {
try {
String path = "src/head/first/decorator2/test.txt";
File file = new File(path);
// 这些代码我们都很熟悉
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(file));
InputStream inputStream = new LowerCaseInputStream(bufferedInputStream);
int c;
// //每读取到一个字节,打印出来
while ((c = inputStream.read()) >= 0) {
System.out.print((char) c);
}
// byte[] buff = new byte[1024];
// int len = inputStream.read(buff);
// System.out.println(new String(buff, 0, len));
inputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
![](https://img.haomeiwen.com/i9064767/5b465ad8d3f955fc.png)
运行结果
hello every one!
OO 原则
- 封装变化
- 多用组合,少用继承
- 针对接口编程,不针对实现变成
- 为交互对象之间的松耦合设计而努力
- 对扩展开放,对修改关闭
现在我们已经掌握了三个设计模式,策略 观察者 装饰者模式
网友评论