装饰者模式——动态扩展对象功能

176 阅读4分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

装饰者模式(Decorator Pattern)的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。

引言

装饰者,顾名思义就是为了装饰或修饰某个事物的东西。我们日常生活中,经常接触到装饰者相关的场景,比如身上的衣服(装饰人),新房的装修等。装饰,并不改变原来的内容,只是为了达到更好的效果加上额外的功能。

在软件系统中,装饰者模式就是在不改变现有对象结构的情况下,动态地给该对象增加一些职责。

问题分析

具体介绍装饰者模式之前,我们先看一个买奶茶场景(说到奶茶是不是就不困了呢)。

在奶茶店里有很多奶茶,如珍珠奶茶、奶绿等。每种奶茶可以加不同的配料,如布丁、燕麦等。然后不同的配料加不同的奶茶都可以组成一杯“新”的品类的奶茶。方便起见,我们假定奶茶只有两个方法:

    /**
     * 1.获取奶茶描述
     */
    public String getDescription();

    /**
     * 2.计价
     * @return
     */
    public double cost();

我们可以这样设计:

  • 奶茶类 MilkyTea;每种单品奶茶(不加配料)继承 MilkTea,有:珍珠奶茶 PearlMilkyTea,奶绿 GreenMilkyTea。
  • 配料 Charge;每种配料继承Charge,有:布丁 Pudding,燕麦 Oat。

如何把奶茶和配料组合再一起呢?有些语言可以用多继承方式,如,珍珠布丁奶茶 PearlPuddingMilkyTea,让其继承珍珠奶茶(PearlMilkyTea) 和 布丁(Pudding)。不过,Java并不支持多继承,所以我们的PearlPuddingMilkyTea只能继承一个类,并重写里面的getDescriptioncost方法。

因为奶茶和配料可以任意搭配,如果按着这样的设计,我们将要创建非常多的类,如 珍珠燕麦奶茶类PearlOatMilkyTea,布丁奶绿 GreenPuddingMilkyTea,燕麦奶绿 GreenOatMilkyTea,珍珠燕麦布丁奶茶 PearlOatPuddingMilkyTea... 如果要新增一种配料,又要重新排列组合了!这样也就产生了「类爆炸」。

针对这样的场景,装饰者模式可以大大简化其设计!

装饰者模式的结构

通常装饰者模式有4个角色

  1. 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
  2. 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  3. 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  4. 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

各角色关系的类图:

image.png

装饰者模式的核心:装饰者和被装饰者都继承同一个抽象构件;同时,装饰者持有一个被装饰者的引用,从而在装饰者内部达到修饰或增强被装饰者的目的。

具体实现

一、抽象构件

// 奶茶抽象类
public abstract class MilkyTea {
    /**
     * 获取奶茶描述
     */
    public abstract String getDescription();

    /**
     * 计价
     * @return
     */
    public abstract double cost();
}

二、具体构件

// 奶绿
public class GreenMilkyTea extends MilkyTea {
    @Override
    public String getDescription() {
        return "奶绿";
    }

    @Override
    public double cost() {
        return 11;
    }
}
// 珍珠奶茶
public class PearlMilkyTea extends MilkyTea {
    @Override
    public String getDescription() {
        return "珍珠奶茶";
    }

    @Override
    public double cost() {
        return 11;
    }
}

三、抽象装饰

// 奶茶所加料抽象类
public abstract class Charge extends MilkyTea{

    // 持有被装饰者引用
    protected MilkyTea milkyTea;

    public Charge(MilkyTea milkyTea){
        this.milkyTea = milkyTea;
    }
}

四、具体装饰

/**
 * 布丁
 */
public class Pudding extends Charge{

    public Pudding(MilkyTea milkyTea){
        super(milkyTea);
    }


    @Override
    public String getDescription() {
        return super.milkyTea.getDescription() + "+布丁" ;
    }

    @Override
    public double cost() {
        return super.milkyTea.cost() + 2;
    }
}
/**
 * 燕麦
 */
public class Oat extends Charge {
    public Oat(MilkyTea milkyTea) {
        super(milkyTea);
    }

    @Override
    public String getDescription() {
        return super.milkyTea.getDescription() + "+燕麦";
    }

    @Override
    public double cost() {
        return super.milkyTea.cost() + 1;
    }
}

五、客户端使用

public class Client {
    public static void main(String[] args) {
        // 珍珠奶茶
        MilkyTea milkyTea = new PearlMilkyTea();
        // 珍珠奶茶加布丁
        milkyTea = new Pudding(milkyTea);

        System.out.println(milkyTea.getDescription() + " " + milkyTea.cost());

        // 再加燕麦
        milkyTea = new Oat(milkyTea);
        System.out.println(milkyTea.getDescription() + " " + milkyTea.cost());

    }
}
// 运行结果:
珍珠奶茶+布丁 13.0
珍珠奶茶+布丁+燕麦 14.0

通过装饰者模式,我们就可以灵活组合奶茶和配料。此外,即使新增一种配料,只需要新增一个配料类,其他地方都不需要额外修改。说明其完全遵守了我们的开闭设计原则。

总结

装饰者模式是一种结构型模式。细心的同学应该会发现它与前面介绍的两种结构型设计模式(《适配器模式:快速兼容旧接口》《桥接模式——独立多维度变化的解决方案》)非常相似,核心部分都是持有一个对象引用。但其引用对象的功能完全不一样:

  • 适配器模式是对旧接口的引用;

  • 桥接模式是对其他维度对象的引用;

  • 装饰者模式是对被装饰者对象的引用