序
这个模式用做饭来说确实感觉不错,因为书上也是这么打比方的,就是用咖啡,还都是那么绕口的名字,感觉实在受不了。所以此处用面条来打比方,原理相同,但我觉得这个应该大家会更熟悉一些。
1、背景
有各式各样的面条,拉面,手擀面,宽条面,面片等等,当然,每种面条由于制作工艺不同,所以价钱也不一样。同时,面条还要配着卤汁,汤料,所以可以加猪骨高汤,鸡架高汤,蔬菜高汤,羊肉汤,或者炸好的甜面酱,西红柿鸡蛋酱等等。同样,加不同的酱料,价钱也不同。
那么首先想到的是,先做一个面条基类,然后从这个类中派生出各种面条类,比如鸡架高汤手擀面,西红柿鸡蛋宽条面。可是,一旦你这样做了,将会出现以下问题:
1)将会出现无穷多的子类,n种类型的面条×m中类型的卤汁=m×n种类型的产品(为了和不同形状的面条区分,凡是上桌的都叫做“产品”)
2)无法多种材料进行拼凑。比如你想吃拉面,配炸酱+蔬菜高汤,那这种设计方法做不到,除非再新建一个类,这样无疑又增大了工作量。
3)无法修改价钱。目前物价越来越高,可能半年后,西红柿从原来的2块一斤,涨到了4块一斤,那么你必须修改所有加了卤汁为西红柿炒鸡蛋的产品类——估计累晕。
所以经过上面问题的讨论,还是采用装饰模式比较好。那么装饰模式长啥样?不要走开,精彩继续。
2、装饰模式
顾名思义,装饰模式就是在原有的基础上,对原有的事物锦上添花。每次加一个装饰,一方面扩展了原有对象的特点,同时,原有对象的基本特质保持不变。即咖啡再怎么变,也还是咖啡,无论你加了糖、奶、豆浆。面条也是,你加什么卤汁,汤料,仍然还是面条。只不过面条的味道变了,口感变了,价钱也变了。上代码看下:
public class Test1 {
public static void main(String args[])
{
/*宽条面西红柿鸡蛋面 2+2=4
* 拉面羊肉汤面 3+3=6
* 宽面条西红柿鸡蛋,羊肉汤面 2+2+3=7
* */
WideNoodles wideNoodles1=new WideNoodles();//2
TomatoEgg tomatoEgg1=new TomatoEgg(wideNoodles1);//2
WideNoodles wideNoodles2=new WideNoodles();//2
TomatoEgg tomatoEgg2=new TomatoEgg(wideNoodles2);//2
GoatSoup goatSoup2=new GoatSoup(tomatoEgg2);//3
StretchNoodles stretchNoodles1=new StretchNoodles();//3
GoatSoup goatSoup1=new GoatSoup(stretchNoodles1);//3
System.out.println(tomatoEgg1.getDes()+tomatoEgg1.price());
System.out.println(goatSoup1.getDes()+goatSoup1.price());
System.out.println(goatSoup2.getDes()+goatSoup2.price());
}
}
abstract class Noodles //面条抽象类
{
protected String description="noodles";
public abstract double price();
public String getDes(){ return description; }
}
//各种面条
class WideNoodles extends Noodles//宽条面
{
public WideNoodles() {this.description="WideNoodles";}
public double price() {return 2.0;}
}
class StretchNoodles extends Noodles//拉面
{
public StretchNoodles() {this.description="StretchNoodles";}
public double price(){ return 3.0;}
}
//卤汁类
class Bittern extends Noodles////基本卤汁
{
protected Noodles noodles;//面条
public Bittern(Noodles n){this.description="Bittern"; this.noodles=n;}//修改成卤汁类型
public double price(){return this.noodles.price()+1.0;}//面条+1块钱卤汁
public String getDes() {return this.description+","+this.noodles.getDes();}//面条+卤汁
}
//具体卤汁,由于上面卤汁基类的方法都已经重新,所以只需要修改price即可
class TomatoEgg extends Bittern//西红柿炒鸡蛋卤汁
{
public TomatoEgg(Noodles n) {super(n); this.description="TomatoEgg";}
public double price(){return this.noodles.price()+2.0;}
}
class GoatSoup extends Bittern//羊肉汤卤汁
{
public GoatSoup(Noodles n) {super(n);this.description="GoatSoup";}
public double price() {return this.noodles.price()+3.0;}
}
class ChickSoup extends Bittern//鸡架卤汁
{
public ChickSoup(Noodles n) {super(n);this.description="ChickSoup";}
public double price() {return this.noodles.price()+4.0;}
}
开始解析上面的代码。刚看到工作的同学准备招聘高级java程序员。读研的伤不起啊。。。
1)从程序中可以看到,所有的类都有一个共同的抽象基类,即Noodles,因为产品无论怎么变化,本质是面条,也应正了继承是Is—a的规律。面条都有描述和价钱。
2)每一种面条都作为独立的个体,而卤汁不一样,卤汁需要依附于面条,也就是有了面条,卤汁才能修饰作为装饰者,去修饰面条。所以卤汁中包含了面条主体。而卤汁+面条=面条,所以面条的本质没有变。
(注意修饰和被修饰的关系,这样才能确定组合的情况)
3、Java中的具体实现
在Java.io.*中,就是这样的道理。所有的IO类都有一个共同的基类,表明这些类都是I/O操作,本质没有变。但是,对于真正做读写操作的(就好像本文中的面条),作为一个独立实体,这些类只能一次读取一个字节,或者一行。而修饰这些进行实际读写操作的装饰者,则可能只是封装了一个缓存,或者将读取的字符转换成小写等等。
4、其他方面
其实说的再远一点。Unix中的管道也是这种思想,每个程序都提供了读取数据和写出数据的接口,然后通过管道,可以一次又一次的传递数据,比如:ls /root | more | grep "Noodles"。当然,more只是为了说明,实际没啥用。