目录
一,背景
喝咖啡是现代年轻人很喜欢的一种休闲方式。按种类,咖啡可以分为摩卡(Mocha),拿铁(Latte)等,而每一种咖啡又可以添加一种或多种佐料就成了新的口味,比如说有的人喜欢加糖,而有的人喜欢加糖的同时又加牛奶。
隔壁街的咖啡馆马上就要开张了,老板听说你是一位优秀的“设计师”,所以想把他们家点咖啡的系统交给你来做,好处就是以后你来点咖啡都可以享受七折的优惠。于是,为了拿下这喝咖啡的好处,你开始思考如何完成这个系统,首先咖啡肯定包含有其描述信息,表明这到底是哪种咖啡?同时还应该有一个方法返回其价格,于是很容易你完成了这样一个类:
public abstract class Coffee {
/**
* 获取coffee的描述信息
* @return
*/
public abstract String getDescription();
/**
* 获取coffee的价格
* @return
*/
public abstract Float getCost();
}
这样做的好处就是:有什么咖啡只需要继承上面的咖啡基类,然后重写相应的描述和价格方法就行了,比如下面:
但是你突然发现一个问题,那就是咖啡的种类真是太多了,也怪人的口味太不一样。要是真的每一种咖啡都生成一个对应的类,如加牛奶的摩卡咖啡,加冰的摩卡咖啡,加巧克力的摩卡咖啡,加牛奶的拿铁咖啡等等,这样产生的类真是太多了,真的就是类爆炸!!!
但是这肯定难不倒聪明的你,从上面加不加巧克力,加冰与否,你产生了一个点子,可不可以给coffee类添加相应的“口味属性”,当为true表示有,当为false则代表不含有该调味品,而咖啡基类在设计时只需要充分考虑到所有的口味就行了,如下所示:
package com.mytest.other;
public abstract class Coffee {
//是否加冰
protected Boolean hasIce;
//是否加牛奶
protected Boolean hasMilk;
//是否加巧克力
protected Boolean hasChocolate;
//...还有其他一些属性
/**
* 获取coffee的描述信息
* @return
*/
public abstract String getDescription();
/**
* 获取coffee的价格
* @return
*/
public abstract Float getCost();
public Boolean getHasIce() {
return hasIce;
}
public void setHasIce(Boolean hasIce) {
this.hasIce = hasIce;
}
public Boolean getHasMilk() {
return hasMilk;
}
public void setHasMilk(Boolean hasMilk) {
this.hasMilk = hasMilk;
}
public Boolean getHasChocolate() {
return hasChocolate;
}
public void setHasChocolate(Boolean hasChocolate) {
this.hasChocolate = hasChocolate;
}
}
但是这样做也出现了个问题,那就是继承了该基类的咖啡派生类,自带有咖啡的基础属性,虽然达到了复用的好处,但是却带来了一些麻烦,比如热咖啡,里面出现是否加冰的属性是否适合?但是当然我们可以通过设置其为false来解决;但是更为严重的是,当咖啡馆新推出了一种新的口味,这就意味着你需要修改基类添加上相应的配料属性,同时带来了问题,父类发生变化,子类也需要修改相应的描述信息,以及价格的计算方式,这样一来违反了开闭原则,导致程序员大部分的时间都浪费在了类的维护上,那么有没有好的方法能解决呢?
装饰模式:在不改变原有类的基础上,给类添加新的职责。
二,探讨
2.1 装饰模式的使用
对于上述的问题,很适合使用装饰模式来完成。因为不管咖啡之中添加了什么奇奇怪怪的东西,但是并不影响它是咖啡的本质,所以我们可以设计一个装饰类,它专门用于处理这些复杂的配料问题。如下:声明基本的咖啡基类
public abstract class Coffee {
/**
* 获取coffee的描述信息
* @return
*/
public abstract String getDescription();
/**
* 获取coffee的价格
* @return
*/
public abstract Float getCost();
}
这与上面并没有什么区别。我们只需要声明获取咖啡描述及其价格的方法即可,然后具体的行动交给咖啡的派生类来处理。
声明具体的咖啡产品,例如最常见的摩卡咖啡和拿铁咖啡。
摩卡咖啡我们假设其价格为5元,如下:
public class MochaCoffee extends Coffee {
private static final String description = "摩卡咖啡";
@Override
public String getDescription() {
return description;
}
@Override
public Float getCost() {
return 5.0f;
}
}
拿铁咖啡假设其价格为6.5元,如下:
public class LatteCoffee extends Coffee {
private final String description = "拿铁咖啡";
@Override
public String getDescription() {
return description;
}
@Override
public Float getCost() {
return 6.5f;
}
}
如果还有其他的咖啡,那么只需要照着上面的例子从Coffee类派生即可,所有的咖啡只负责咖啡最基本的口味,而配料则由相应的装饰类来完成。
声明一个咖啡的装饰类叫做CoffeeDecorate,它需要继承自咖啡类,以保证它和coffee具有相同的动作,如下:
public abstract class CoffeeDecorate extends Coffee {
@Override
public String getDescription() {
return "咖啡装饰器";
}
}
然后从咖啡装饰类派生出具体的口味,不同的是它需要持有一个Coffee对象,因为咖啡装饰类,面向的对象就是咖啡,如下,我们声明一个加冰,和加牛奶的咖啡装饰类,
加冰:
/**
* 加冰的咖啡
*/
public class IceCoffeeDecorate extends CoffeeDecorate {
//对具体的coffee进行装饰
private Coffee coffee;
public IceCoffeeDecorate(Coffee coffee){
this.coffee = coffee;
}
/**
* 加冰额外收5毛
* @return
*/
@Override
public Float getCost() {
return coffee.getCost() + 0.5f;
}
/**
* 获取的coffee信息带上加冰
* @return
*/
@Override
public String getDescription() {
return coffee.getDescription() + "--加冰";
}
}
加牛奶:
/**
* 牛奶装饰类
*/
public class MilkCoffeeDecorate extends CoffeeDecorate {
private Coffee coffee;
public MilkCoffeeDecorate(Coffee coffee){
this.coffee = coffee;
}
/**
* 加牛奶,多收1元
* @return
*/
@Override
public Float getCost() {
return coffee.getCost() + 1;
}
@Override
public String getDescription() {
return coffee.getDescription() + "--加牛奶";
}
}
最后到了我们测试的时候,让我们“趁热”生产一个加冰,又加牛奶的摩卡咖啡吧:
public class CoffeeTest {
@Test
public void test(){
//先生产摩卡咖啡
Coffee coffee = new MochaCoffee();
//然后对摩卡咖啡进行装饰
//先加牛奶
coffee = new MilkCoffeeDecorate(coffee);
//再加冰
coffee = new IceCoffeeDecorate(coffee);
//获取咖啡的名称
System.out.println(coffee.getDescription());
//获取咖啡的加个
System.out.println(coffee.getCost());
}
}
然后,看看具体的效果:
摩卡咖啡--加牛奶--加冰
6.5
相应的类图如下:
这样做的好处是什么呢?你不需要修改具体Coffee类的前提下,可以使用装饰类扩充其功能,而且你可以在一个装饰器下再嵌套另一个装饰器,俗称套娃,使Coffee更为强大。
2.2 jdk中的装饰模式
提到java中装饰模式的使用,就不得不提到java的io了,如下是我们最常见的用法:
@Test
public void testIo() throws Exception{
InputStream in = new FileInputStream("a.txt");
BufferedInputStream bis = new BufferedInputStream(in);
bis.close();
}
首先获取一个输入流对象,然后对其进行装饰,得到一个带缓冲的输入流,BufferedInputStream具体的类图如下:
从上面的设计可以得出InputStream就是被装饰的对象,而FilterInputStream就是具体的装饰类。我们可以参考,实现一个自己的字节流包装类,如下:
public class UpperCaseInputStream extends FilterInputStream {
protected UpperCaseInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int c = super.read();
return -1 == c ? c : Character.toUpperCase((char)c);
}
@Override
public int read(byte[] b) throws IOException {
return super.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int num = super.read(b, off, len);
if(-1 == num)
return num;
for(int i = 0; i < num; i++){
b[i] = (byte)Character.toUpperCase((char)b[i]);
}
return num;
}
}
这个字节流包装类的作用便是将读到的字符全部转为大写,在使用时我们可以这样来做:
@Test
public void test1() throws Exception{
InputStream in = new FileInputStream("D://a.txt");
in = new UpperCaseInputStream(in);
in = new BufferedInputStream(in);
byte[] bytes = new byte[1024];
int result;
while((result = in.read(bytes)) != -1){
for(int i = 0; i < result; i++)
System.out.print((char)bytes[i]);
}
in.close();
}
在这里有两个问题需要注意:
(1)read方法为什么返回的是int呢?而不应该是byte的嘛
刚开始我也不理解,最后发现是因为byte是带符号的,范围在-128~127,我们知道在判断是否读完的时候是用 -1 来进行判断的,但这就有个问题,万一某一个byte二进制表示就是-1 呢?此处没办法进行判断,所以采用了int。当用int时,如果读出来是-1,在其高位进行补零,结果就是正的,不影响使用-1来做判断的标志。
(2)java中默认采用Unicode编码,即2个字节表示一个字符,char类型的表示范围是0~65535,是没有负数的,这是Unicode编码的规定,Unicode规定了怎样来表示不同字符的编码,但是没有说明如何进行存储,而utf-8编码是一种可变长的编码方式,它是Unicode编码的具体实现。
三, 总结
装饰模式可以动态地给对象添加上新的职责,而不去修改原有的代码,它比继承更为灵活,同时具有弹性;缺点就是这种“套娃”模式在使用的时候不方便,因为装饰类的膨胀,需要开发者知道都有哪些装饰类,而且得知道它们不同的功能,增加了开发的难度。