设计模式学习(三)——装饰者模式
一.需求
小王的便利店里卖柴鸡蛋,而且很受欢迎,一下子让小王夫妇打开了思路:原来只要细心观察小区居民的需求,可以卖的东西还有很多。小王的媳妇儿小芳琢磨着在便利店门口卖煎饼果子!小区里住着不少上班族,他们每天早上急匆匆去上班,一个热乎的煎饼果子又营养,又美味,一定会受到大家喜欢的!
说干就干,一周后,小芳的煎饼摊就正式开始营业了。果然,不一会就排起来长队……一份标准的煎饼果子是5元,放一个鸡蛋,一块薄脆,再撒上葱花,香菜,小芳又别处心裁,加了少许榨菜,咬一口,薄脆的酥脆,煎饼的松软,再加上榨菜的清爽……绝了!
民以食为天,小小的煎饼果子也能吃出不少名堂。有的想多加一个鸡蛋,有的想要两块薄脆,有的想多加一根火腿……不一而足,加一个鸡蛋要多加5角,加一块薄脆5角,加一个火腿1元,大家口味各异,把小芳忙的不可开交。算账容易出错不说,还大大影响了效率。
小王不愧是程序员出身,看到老婆的难处,当即决定给老婆写一个软件用来给煎饼果子收款。
二.初步尝试
尝试一
在仔细分析了面临的问题之后,小王觉得:问题主要在于很多顾客想要一些“豪华版”的煎饼果子,例如加一个鸡蛋,加一个火腿,加一块薄脆,在标准的煎饼果子之外,只要把常见的搭配设计好就可以了。小王的设计如下:
// 标准煎饼果子
class ChineseHamburger {
private static final float price = 5.0f;
public float cost() {
return price;
}
}
// 标准煎饼果子加一份薄脆
class CHAddCrisp extends ChineseHamburger {
private static final float price = 0.5f;
public float cost() {
return super.cost() + price;
}
}
// 标准煎饼果子加一个鸡蛋
class CHAddEgg extends ChineseHamburger {
private static final float price = 1.0f;
public float cost() {
return super.cost() + price;
}
}
// 标准煎饼果子加一根火腿
class CHAddHam extends ChineseHamburger {
private static final float price = 1.0f;
public float cost() {
return super.cost() + price;
}
}
public class ChineseHamburgerAdmin {
public static void main(String[] args) {
ChineseHamburger ch = new ChineseHamburger();
System.out.println("标准煎饼果子售价" + ch.cost());
ChineseHamburger chAddEgg = new CHAddEgg();
System.out.println("标准煎饼果子加鸡蛋售价" + chAddEgg.cost());
ChineseHamburger chAddCrisp = new CHAddCrisp();
System.out.println("标准煎饼果子加薄脆售价" + chAddCrisp.cost());
}
}
类图如下:
运行程序:
标准煎饼果子售价5.0
标准煎饼果子加鸡蛋售价6.0
标准煎饼果子加薄脆售价5.5
这样就大大方便卖夹饼果子了,只要根据客户的需要选择相应的煎饼果子,就可以直接显示价格,不用自己算了,媳妇用着很顺手,小王脸上乐开了花……
没过多久,新的问题又出现了:有的客户想加两根火腿,有的客户想加一个鸡蛋再加一个火腿……客户的需求总是多种多样,又让小王的媳妇应接不暇。
看来,之前设计的那些组合根本不够用,难道还要再加CHAddHamAddEgg、CHAddTwoHam……?显示这不是一个理想的方案,天知道顾客们还会有什么特殊的口味呢?
尝试二
// 标准煎饼果子
class ChineseHamburger {
private static final float PRICE = 5.0f; // 煎饼果子价格
private static final float CRISP_PRICE = 0.5f; // 薄脆价格
private static final float EGG_PRICE = 1.0f; // 鸡蛋价格
private static final float HAM_PRICE = 1.0f; // 火腿价格
private int addCrispNum; // 需要加多少薄脆
private int addEggNum; // 需要加多少鸡蛋
private int addHamNum; // 需要加多少火腿
public ChineseHamburger(int addCrispNum, int addEggNum, int addHamNum) {
this.addCrispNum = addCrispNum;
this.addEggNum = addEggNum;
this.addHamNum = addHamNum;
}
// 计算煎饼以及附加的原料的价格
public float cost() {
return PRICE
+ addCrispNum * CRISP_PRICE
+ addEggNum * EGG_PRICE
+ addHamNum * HAM_PRICE;
}
}
public class ChineseHamburgerAdmin {
public static void main(String[] args) {
ChineseHamburger ch = new ChineseHamburger(0, 0, 0);
System.out.println("标准煎饼果子售价" + ch.cost());
ChineseHamburger chAddEgg = new ChineseHamburger(0, 1, 0);
System.out.println("标准煎饼果子加鸡蛋售价" + chAddEgg.cost());
ChineseHamburger chAddCrisp = new ChineseHamburger(1, 0, 0);
System.out.println("标准煎饼果子加薄脆售价" + chAddCrisp.cost());
ChineseHamburger chAddCrispAddEgg = new ChineseHamburger(1, 1, 0);
System.out.println("标准煎饼果子加薄脆加鸡蛋售价" + chAddCrispAddEgg.cost());
}
}
这个方案比之前的方案代码少了很多,既避免了类爆炸,又使得逻辑更灵活,现在随便加多少鸡蛋,加多少火腿都可以从容应对了。
谁知,又过了几周,大家对加火腿,加鸡蛋渐渐的也吃腻了,顾客就是上帝,这帮上帝可是真难伺候啊!小王和媳妇绞尽脑汁想丰富口味,他们又尝试了在煎饼里加肉松,加培根,加土豆丝……
但是这样一来,之前的程序就又需要修改了:需要在ChineseHamburger类中再添加AddMeatFlossNum(加肉松数量)、AddBaconNum(加培根数量)、AddPotatoesNum(加土豆丝数量),并且需要修改cost方法。
这无疑违反了编程的一个基本原则:开闭原则。即代码应该对扩展开放,对修改关闭。换句话说,每当逻辑升级,最好通过新增代码实现,而不修改现有代码。
三.更好的方案
下面我们来看另一种实现。
// 抽象类:面饼,可以是煎饼果子,也可以是卷饼,手抓饼等等
abstract class Biscuit {
abstract float cost();
}
// 煎饼果子继承面饼抽象类
class ChineseHamburger extends Biscuit{
private static final float PRICE = 5.0f; // 煎饼果子价格
@Override
public float cost() {
return PRICE;
}
}
// 面饼装饰者,针对当前场景,没有声明其他方法,只继承Biscuit中的方法
abstract class BiscuitDecorator extends Biscuit{
}
// 面饼装饰者之一:薄脆
class Crisp extends BiscuitDecorator {
private static final float PRICE = 0.5f;
// 需要装饰的面饼
private Biscuit biscuit;
public Crisp(Biscuit biscuit) {
this.biscuit = biscuit;
}
@Override
float cost() {
return biscuit.cost() + PRICE;
}
}
// 面饼装饰者之一:鸡蛋
class Egg extends BiscuitDecorator {
private static final float PRICE = 1.0f;
// 需要装饰的面饼
private Biscuit biscuit;
public Egg(Biscuit biscuit) {
this.biscuit = biscuit;
}
@Override
float cost() {
return biscuit.cost() + PRICE;
}
}
// 面饼装饰者之一:火腿
class Ham extends BiscuitDecorator {
private static final float PRICE = 0.5f;
// 需要装饰的面饼
private Biscuit biscuit;
public Ham(Biscuit biscuit) {
this.biscuit = biscuit;
}
@Override
float cost() {
return biscuit.cost() + PRICE;
}
}
public class ChineseHamburgerAdmin {
public static void main(String[] args) {
Biscuit ch = new ChineseHamburger();
System.out.println("标准煎饼果子售价" + ch.cost());
// 用鸡蛋装饰煎饼果子
Biscuit chAddEgg = new Egg(ch);
System.out.println("标准煎饼果子加鸡蛋售价" + chAddEgg.cost());
// 用薄脆装饰煎饼果子
Biscuit chAddCrisp = new Crisp(ch);
System.out.println("标准煎饼果子加薄脆售价" + chAddCrisp.cost());
// 用薄脆装饰已经加了鸡蛋的煎饼果子
Biscuit chAddCrispAddEgg = new Crisp(chAddEgg);
System.out.println("标准煎饼果子加薄脆加鸡蛋售价" + chAddCrispAddEgg.cost());
}
}
当前这种实现的类图如下:
这种实现的好处在于:灵活、扩展性强。煎饼果子和煎饼果子装饰者都继承自面饼类,装饰者可以随意对煎饼果子进行装饰,如果需要加肉松,之前的代码都无需修改,只要再实现一个肉松类继承BiscuitDecorator即可。同样,如果以后想卖卷饼、手抓饼也没问题,只要实现卷饼类实现Biscuit ,然后用装饰者装饰就可以了。
四.模式总结
想必大家已经看出来了,我们最后使用的方式就是装饰者模式了。
使用场景
当需要动态地增加责任和行为到对象上时。
例如我们都很熟悉了java IO类就大量的使用了装饰者模式,我们一般这样来声明一个输入流:
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("file.txt")));
使用InputStreamReader类装饰FileInputStream类,从而完成字节流到字符流的转换,再用BufferedReader类装饰InputStreamReader,实现有缓冲的读,优化性能。
类图
在装饰者模式中,主要有两种角色:组件和装饰者,装设者和组件继承自同一个父类,因此装饰者可以任意装饰其他组件,充分的利用多态的特性来进行行为扩展。
优点
1.可扩展性,添加新的行为无需修改已有代码
2.灵活性,可以根据自己的需求随意对组件进行装饰,动态的改变行为
缺点
1.会增加很多小类,每一种装饰行为需要实现一个特定的装饰类,增加维护成本
2.如果装饰者链很长,则增加了程序的复杂性,出现问题时排查成本高
参考资料:
1.《Head First设计模式》
2.设计模式读书笔记-----装饰者模式
本文已迁移至我的博客:http://ipenge.com/28811.html