背景案例介绍
以奶茶为例,如下图所示,Beverage(饮料)是一个抽象类,类里面有一个description(描述)实例用来描述该奶茶,可以描述出奶茶的分类,奶茶有很多种分类,比如抹茶,波霸,玛奇朵,奶绿等等。还有一个cost()方法,用于给子类继承,实现每种奶茶的各自价格.
众所周知,我们每个人去买奶茶,可能还需要往奶茶里面加一些配料,如红豆,燕麦,布丁,奶霜等等,但是每种配料的价格也不尽相同,每款奶茶的价格也不一定相同,所以刚开始OO设计师就得设计出无数个类(因为我每种配料可以加多份,如我加两份红豆和3份燕麦,而这得加钱),分别用于得到每种组合的价格,如红豆抹茶奶茶、红豆波霸奶茶、布丁燕麦玛奇朵奶茶…如果奶茶分类和配料有很多的话,就会导致类多到爆炸.而且还存在一个问题,并且如果某天需要修改红豆的价格,完蛋了,需要找到所有带有红豆的奶茶类,然后进入代码一个个修改每个奶茶的cost方法。这样就严重的违反了设计模式的开闭原则.下图可以生动形象的体现出类爆炸的情况.
......上面也说了,设计师需要设计的类远不止这些,因为你没法确定顾客要加哪些配料,每种配料要加几份.所以聪明的OO设计师们就想到了后文的装饰者设计模式.
装饰者模式
装饰者模式的作用
装饰者模式的主要作用就是实现了开闭原则,即对功能扩展开发,对代码修改关闭.
定义:动态给一个对象添加一些额外的职责,就象在墙上刷油漆.使用Decorator装饰者模式相比用生成子类方式(继承)达到功能的扩充显得更为灵活。
设计初衷:通常可以使用继承来实现功能的拓展,如果这些需要拓展的功能的种类很繁多,那么势必生成很多子类,增加系统的复杂性,同时,使用继承实现功能拓展,我们必须可预见这些拓展功能,这些功能是编译时就确定了的,是静态的。所以为了避免这些问题,装饰者模式就出现了.
注意: 装饰者与被装饰者拥有共同的超类,继承的目的是继承类型,而不是行为.
对我们初学者来说,我们可以对已经存在的核心对象称之为被装饰者,对这些核心对象进行扩展的对象称之为装饰者.
以奶茶为例,奶茶的分类有抹茶,波霸,玛奇朵,奶绿等等,这些就相当于是被装饰者,而且每一种奶茶都可以加一些配料,如红豆,燕麦,布丁等等,这些配料就相当于是装饰者.
装饰者模式的实现
我们可以将奶茶的品种,抹茶、波霸、玛奇朵、奶绿都继承Beverage类,都重写一下,然后用Decorator继承Beverage为一个装饰者类(也是抽象的类,同时注意我们上面也提了,我们继承的目的),然后红豆、奶绿、燕麦等调料都继承了Decorator类(装饰者类),在每个装饰者类里面都存放了一个Beverage对象,也就是用来装我们的抹茶、波霸等奶茶的。
说明:编写代码的时候我只编写了一种奶茶类和一种配料类,因为其它奶茶分类和配料类均类似,只是修改一下对应的描述和价格就可以了.
package com.deco;
/**
* @Author 作者:小龙猿
* @Project 项目:decoration
* @Time 时间:2021/9/4 10:08
*/
public abstract class Beverage {
String description = "";
public String getDescription(){//用于子类继承,得到每种款式的名称
return description;
}
public abstract double cost();//用于子类继承,实现各自的价格
}
定义被装饰者
package com.deco;
import com.deco.Beverage;
/**
* @Author 作者:小龙猿
* @Project 项目:decoration
* @Time 时间:2021/9/4 10:14
*/
//被装饰者类
public class BoBa extends Beverage {
public BoBa(){
description = "波霸奶茶";
}
@Override
public double cost() {
return 10;//定义波霸奶茶的价格为10元
}
}
定义装饰者
package com.deco;
/**
* @Author 作者:小龙猿
* @Project 项目:decoration
* @Time 时间:2021/9/4 10:19
*/
//装饰者类
public abstract class Decorator extends Beverage{
Beverage beverage;
public abstract String getDescription();
}
package com.deco;
/**
* @Author 作者:小龙猿
* @Project 项目:decoration
* @Time 时间:2021/9/4 10:21
*/
//红豆类
public class HD extends Decorator {
public HD(Beverage beverage){
this.beverage = beverage;
}
@Override
public String getDescription() {
return beverage.getDescription()+"红豆";
}
@Override
public double cost() {
return beverage.cost()+2;//原有基础上加了一份红豆的钱2元
}
}
测试结果
package com.deco;
/**
* @Author 作者:小龙猿
* @Project 项目:decoration
* @Time 时间:2021/9/4 10:25
*/
//测试类
public class TestDecorate {
public static void main(String args[]){
Beverage beverage = new BoBa();//只要一份波霸奶茶
System.out.println(beverage.getDescription()
+"--"+beverage.cost()+"元");
Beverage beverage1 = new BoBa();//要一份波霸奶茶
beverage1 = new HD(beverage1);//要加一份红豆
beverage1 = new HD(beverage1);//再加一份红豆
//beverage1 = new NS(beverage1);//再加一份奶霜,这儿没有实现这个类,只是说明一下用法
System.out.println(beverage1.getDescription()
+"--"+beverage1.cost()+"元");
}
}
第一位顾客点了一杯波霸奶茶,什么配料都没有加,价格就是10元;
第二位顾客点了一杯波霸奶茶,还加了两份红豆,价格就是10+2+2=14元
- 每当红豆价格改变的时候我们只需要修改红豆(装饰者HD)的价格就可以了
- 如果增加配料(装饰者),我们只需要增加一个类来继承Decorator类就可以了,同样可以用来装饰
- 如果来了一种新饮品(被装饰者),如奶绿奶茶,就直接建立一个奶绿奶茶类继承Beverage,就可以了
- 添加多份配料,如两份红豆,上面已经解决.
Java中已存在的装饰者模式
Java中的I/O,我们知道InputStream,FileInputStream,BufferedInputStream …等等,其实I/O就是使用的装饰者模式,只要继承FIlterInpiutStream类,就可以作为装饰者了.
我们现在就来实现一个装饰者类,功能是将文件中的大写字母转小写字母.
定义装饰者类:
package com.deco;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @Author 作者:小龙猿
* @Project 项目:decoration
* @Time 时间:2021/9/4 11:05
*/
//自定义一个装饰者类,用于将大写字母转小写
public class LowerCaseInputStream extends FilterInputStream {
protected LowerCaseInputStream(InputStream in) {
super(in);
}
//重写父类的read方法
public int read() throws IOException{
int c = in.read();
return (c == -1 ? c : Character.toLowerCase((char) c));//只要还能读到数据,就把该数据变成小写字母
}
}
测试:
package com.deco;
import java.io.*;
/**
* @Author 作者:小龙猿
* @Project 项目:decoration
* @Time 时间:2021/9/4 11:13
*/
public class InputDecorationTest {
public static void main(String[] args) throws IOException {
//创建文件
File file = new File("test.txt");
writeDataToFile(file);//向文件写入字母数据
convertToLower(file);//将文件中的大写字母转为小写
}
private static void writeDataToFile(File file) throws IOException {
OutputStream out = null;
try {
out = new BufferedOutputStream(new FileOutputStream(file));
byte[] b = {65,66,67,97,98,99};//ABCabc
out.write(b);
}catch (IOException e){
e.printStackTrace();
}finally {
out.close();
}
}
private static void convertToLower(File file) throws IOException {
int c;
InputStream in = null;
try {
in = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream(file)));
while ((c = in.read()) >= 0)//只要还能读到数据,就大写转小写
System.out.print((char) c);
}catch (IOException e){
e.printStackTrace();
}finally {
if (in != null){
in.close();
}
}
}
}
我们先向一个文件写入了数据,然后再从这个文件读取数据并将其转为小写字母.
文件内容:
控制台结果: