想一个场景
我们在Service里经常会去取个人信息,为了不每次都通过DAO从数据库里面读数据,那我们会在数据库查询之前加上一层缓存读取,返回同样结构的数据且不改变查询逻辑本身,这里添加缓存就是在使用装饰器模式
模式介绍
在模式介绍之前,先声明一条简短的程序设计原则
开闭原则
类应该对扩展开放,对修改关闭
装饰器模式是在对已有逻辑的不修改的情况下,在外层包装一层逻辑,使包装逻辑与原始逻辑去耦
我们确实可以使用子类继承父类,然后在子类中添加包装逻辑,但这儿有一点很关键,就是子类中的包装逻辑与父类逻辑耦合,无法拆分,在另外的子类中需要同样的包装逻辑时,又需要再写一遍。
这就很成问题,同样的代码在不同的地方多次出现,这就意味着这一块儿代码需要改进了。
类图
- ConcreteComponent:要动态的加上包装逻辑的对象
- ConcreteDecoratorA/ConcreteDecoratorB:具体包装逻辑的装饰器
示例
此处我使用了一个售卖咖啡的场景,在用户选择一份咖啡加多份调料的时候,来计算总价格,并输出已点的内容。
- 费用计算接口
public interface CostCalculator {
String getDescription();
int cost();
}
- 调味品接口
public interface Condiment extends CostCalculator {
int getPrice();
}
- 咖啡虚类
public abstract class Coffee implements CostCalculator{
String description = "未知的类型";
public String getDescription(){
return description;
}
}
- 具体咖啡类:浓缩咖啡
public class Espresso extends Coffee {
public Espresso(){
description = "浓缩咖啡";
}
/**
* 单售价格
* @return
*/
@Override
public int cost() {
return 14;
}
}
- 具体咖啡类:混合咖啡
public class HouseBlend extends Coffee {
public HouseBlend(){
description = "混合咖啡";
}
/**
* 单售价格
* @return
*/
@Override
public int cost() {
return 11;
}
}
- 具体调味品:巧克力
public class Chocolate implements Condiment {
private CostCalculator costCalculator;
/**
* 巧克力的价格
*/
private int price = 5;
public Chocolate(CostCalculator costCalculator){
this.costCalculator = costCalculator;
}
@Override
public String getDescription() {
return costCalculator.getDescription()+",巧克力";
}
/**
*
* @return
*/
@Override
public int cost() {
return costCalculator.cost()+getPrice();
}
@Override
public int getPrice() {
return price;
}
}
- 具体调味品:牛奶
public class Milk implements Condiment {
private CostCalculator costCalculator;
/**
* 牛奶的价格
*/
private int price = 6;
public Milk(CostCalculator costCalculator){
this.costCalculator = costCalculator;
}
@Override
public String getDescription() {
return costCalculator.getDescription()+",牛奶";
}
@Override
public int cost() {
return costCalculator.cost()+getPrice();
}
@Override
public int getPrice() {
return price;
}
}
- 单元测试类
public class CoffeeOrderTest {
@Test
public void test1(){
CostCalculator espresso = new Espresso();
//一杯浓缩咖啡的价格
printPrice(espresso.cost());
printDes(espresso.getDescription());
//加上牛奶之后的价格
espresso = new Milk(espresso);
printPrice(espresso.cost());
printDes(espresso.getDescription());
//一杯混合咖啡
CostCalculator houseBlend = new HouseBlend();
printPrice(houseBlend.cost());
printDes(houseBlend.getDescription());
//在加上巧克力后的价格
houseBlend = new Chocolate(houseBlend);
printPrice(houseBlend.cost());
printDes(houseBlend.getDescription());
//加上牛奶之后的价格
houseBlend = new Milk(houseBlend);
printPrice(houseBlend.cost());
printDes(houseBlend.getDescription());
}
private void printPrice(int price){
System.out.println("价格:"+price+"元");
}
private void printDes(String des){
System.out.println("包含:"+des);
}
}
- 控制台输出
结语
-
使用装饰器模式,可以把继承的树形拓展转换为接口的水平拓展
-
就水平拓展能力来看,装饰者模式也存在一个明显的弱点:需要定义大量的小装饰类,这可能会给使用者带来一定的困扰。
-
由于装饰器的装饰次数无限制,所以需要工厂模式和生成器模式来对装饰器的使用进行限制
附属1:Java内置的jar包中的装饰器模式的应用
在java.io包里,对InputStream,OutputStream几乎都是用的装饰器模式来做拓展的
我也可以利用装饰器模式来拓展InputStream
将输入流中的英文字符转换为小写
public class LowerCaseInputStream extends FilterInputStream {
/**
* Creates a <code>FilterInputStream</code>
* by assigning the argument <code>in</code>
* to the field <code>this.in</code> so as
* to remember it for later use.
*
* @param in the underlying input stream, or <code>null</code> if
* this instance is to be created without an underlying stream.
*/
protected LowerCaseInputStream(InputStream in) {
super(in);
}
public int read() throws IOException {
int c = super.read();
return c == -1 ? c : Character.toLowerCase(c);
}
public int read(byte[] b,int offset,int len) throws IOException{
int result = super.read(b,offset,len);
for (int i = offset;i<offset+result;i++){
b[i] = (byte)Character.toLowerCase(b[i]);
}
return result;
}
}
测试类
public class LowerCaseInputStreamTest {
@Test
public void test(){
int c = 0;
try{
LowerCaseInputStream inputStream = new LowerCaseInputStream(
new BufferedInputStream(
new FileInputStream("/data/test.txt")));
while((c = inputStream.read())>=0){
System.out.print((char) c);
}
inputStream.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
读取文件
测试结果
附属2:使用工厂模式和策略模式来优化装饰者模式
在上面使用装饰器的时候已经提到过,大量的小装饰类会淹没使用者。而且对于咖啡的外层使用者来说,如果他需要多次去拿一杯摩卡咖啡,那么他就不用关注摩卡咖啡是通过哪些装饰器产生的。
例如:
抽象工厂类
public abstract class AbstractCoffeeFactory {
public abstract CostCalculator getCoffee();
}
摩卡咖啡工厂
public class MochaCoffeeFactory extends AbstractCoffeeFactory{
private CostCalculator mocha;
public MochaCoffeeFactory(){
init();
}
public CostCalculator getCoffee(){
//加巧克力
mocha = new Chocolate(mocha);
//加牛奶
mocha = new Milk(mocha);
return mocha;
}
private void init(){
mocha = new Espresso();
}
}
咖啡工厂选择器(策略模式)
public class CoffeeSelector {
private Map<Integer,AbstractCoffeeFactory> coffeeFactoryMap;
public CoffeeSelector(){
init();
}
public CostCalculator select(int type){
return coffeeFactoryMap.get(type).getCoffee();
}
private void init(){
coffeeFactoryMap = new HashMap<>();
coffeeFactoryMap.put(CoffeeType.MOCHA,new MochaCoffeeFactory());
}
public @interface CoffeeType{
int MOCHA = 0;
}
}
测试类
/**
* 咖啡订单测试
*/
public class CoffeeOrderTest {
/**
* 装饰器模式
*/
@Test
public void test1(){
CostCalculator espresso = new Espresso();
//一杯浓缩咖啡的价格
printPrice(espresso.cost());
printDes(espresso.getDescription());
//加上牛奶之后的价格
espresso = new Milk(espresso);
printPrice(espresso.cost());
printDes(espresso.getDescription());
//一杯混合咖啡
CostCalculator houseBlend = new HouseBlend();
printPrice(houseBlend.cost());
printDes(houseBlend.getDescription());
//在加上巧克力后的价格
houseBlend = new Chocolate(houseBlend);
printPrice(houseBlend.cost());
printDes(houseBlend.getDescription());
//加上牛奶之后的价格
houseBlend = new Milk(houseBlend);
printPrice(houseBlend.cost());
printDes(houseBlend.getDescription());
}
/**
* 添加工厂模式
*/
@Test
public void test2(){
CoffeeSelector selector = new CoffeeSelector();
CostCalculator mocha = selector.select(CoffeeSelector.CoffeeType.MOCHA);
printPrice(mocha.cost());
printDes(mocha.getDescription());
}
private void printPrice(int price){
System.out.println("价格:"+price+"元");
}
private void printDes(String des){
System.out.println("包含:"+des);
}
}
测试结果
结果没有发生变化