学习设计模式——装饰器模式

2,184 阅读5分钟

概述

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。它是作为现有的类的一个包装。

若要扩展一个对象的功能,装饰器模式提供了比继承更有弹性的替代方案。组合优于继承

何时使用:

  • 需要扩展一个类的功能,或给一个类增加附加责任。
  • 需要动态的给一个对象增加功能,这些功能可以再动态地撤销。
  • 需要增加一些基本功能的排列组合而产生的非常大量的功能,从而使继承变得不现实。

UML 类图:

image.png

角色组成:

  1. 抽象构件角色(Component): 定义可以动态添加任务的对象的接口
  2. 具体构件角色(ConcreteComponent):定义一个要被装饰器装饰的对象,即 Component 的具体实现
  3. 抽象装饰器(Decorator): 持有一个构件(Conponent)对象的实例,并定义一个和抽象构件一致的接口。维护对组件对象和其子类组件的引用
  4. 具体装饰器角色(ConcreteDecorator):向组件添加新的职责

通用代码

抽象构件角色:

public interface Component {
    void operation();
}

具体构件角色:

public class ConcreteComponent implements Component {
    @Override
    public void operation() {
        // 具体业务代码
        System.out.println("ConcreteComponent 通用基础逻辑");
    }
}

抽象装饰角色:

public abstract class Decorator implements Component {
    private Component component;

    public Decorator(Component component) {
        this.component = component;
    }

    @Override
    public void operation() {
        // 委派给构件
        component.operation();
    }
}

具体装饰角色:

public class ConcreteDecorationA extends Decorator {

    public ConcreteDecorationA(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        decorateMethod();
    }

    // 定义自己的修饰逻辑
    private void decorateMethod() {
        System.out.println("ConcreteDecorationA 的修饰逻辑");
    }
}

public class ConcreteDecorationB extends Decorator {

    public ConcreteDecorationB(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        decorateMethod();
    }

    // 定义自己的修饰逻辑
    private void decorateMethod() {
        System.out.println("ConcreteDecorationB 的修饰逻辑");
    }
}

使用:

public class Test {
    public static void main(String[] args) {
        Component component = new ConcreteComponent();
        Decorator decoratorA = new ConcreteDecorationA(component);
        decoratorA.operation();
    }
}

结果:

ConcreteComponent 通用基础逻辑
ConcreteDecorationA 的修饰逻辑

看到这里,相信大家对装饰器模式的通用代码结构清楚了许多,下面我们通过一个实际使用案例来进一步诠释。

实例

相信大家都喝过奶茶,奶茶的种类有很多种,比如有椰香奶茶、QQ奶茶,巧克力奶茶等,奶茶中又可以添加椰果、红豆、布丁等不同的甜品。奶茶店现在要卖各种口味的奶茶,如果不使用装饰模式,那么在销售系统中,各种不一样的奶茶都要产生一个类,如果有5种奶茶类,5种甜品,那么就会产生至少25个类(不包括混合口味),如果使用了装饰模式,那么几个类就可以搞定了。

奶茶接口类

public interface MilkTea {
    // 返回奶茶描述
    String getDescription();
    // 返回价格
    double getPrice();
}

具体的奶茶类:巧克力奶茶、QQ奶茶

public class ChocolateMT implements MilkTea{
    private String description = "巧克力奶茶";
    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public double getPrice() {
        return 15;
    }
}


public class QQMT implements MilkTea {
    private String description = "QQ奶茶";
    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public double getPrice() {
        return 10;
    }
}

抽象装饰类

public abstract class Decorator implements MilkTea {

    protected MilkTea milkTea;

    public Decorator(MilkTea milkTea) {
        this.milkTea = milkTea;
    }

    @Override
    public String getDescription() {
        return milkTea.getDescription();
    }

    @Override
    public double getPrice() {
        return milkTea.getPrice(); // 价格由种类来决定
    }
}

具体装饰类:给奶茶加入椰果

public class Coconut extends Decorator {

    private String description = "加了椰果!";

    public Coconut(MilkTea milkTea) {
        super(milkTea);
    }

    @Override
    public String getDescription() {
        return milkTea.getDescription() + "\n" + description;
    }

    @Override
    public double getPrice() {
        return milkTea.getPrice() + 3;  // 3 表示椰果的价格
    }

}

具体装饰类:给奶茶加入布丁

public class Pudding extends Decorator {

    private String description = "加了布丁!";

    public Pudding(MilkTea milkTea) {
        super(milkTea);
    }

    @Override
    public String getDescription() {
        return milkTea.getDescription() + "\n" + description;
    }

    @Override
    public double getPrice() {
        return milkTea.getPrice() + 5;  // 5表示布丁的价格
    }
}

具体装饰类:给奶茶加入珍珠

public class Pearl extends Decorator {

    private String description = "加了珍珠!";

    public Pearl(MilkTea milkTea) {
        super(milkTea);
    }

    @Override
    public String getDescription() {
        return milkTea.getDescription() + "\n" + description;
    }

    @Override
    public double getPrice() {
        return milkTea.getPrice() + 10;  // 10表示珍珠的价格
    }
}

测试

// 第一种写法
public class Test {
    public static void main(String[] args) {
        // 选择巧克力奶茶
        MilkTea milkTea = new ChocolateMT();
        // 第一次修饰,为巧克力奶茶添加布丁
        milkTea= new Pudding(milkTea);
        // 第二次修饰,为巧克力奶茶添加椰果
        milkTea = new Coconut(milkTea);
        System.out.println(milkTea.getDescription() + "\n加了布丁的巧克力奶茶的价格:" + milkTea.getPrice());
    }
}

// 第二种写法
public class Test {
    public static void main(String[] args) {
        // 选择巧克力奶茶
        MilkTea milkTea = new ChocolateMT();
        // 第一次修饰,为巧克力奶茶添加布丁
        Pudding puddingMilkTea = new Pudding(milkTea);
        // 第二次修饰,为巧克力奶茶添加椰果
        Coconut coconutMilkTea = new Coconut(puddingMilkTea);
        System.out.println(coconutMilkTea.getDescription() + "\n加了布丁的巧克力奶茶的价格:" + coconutMilkTea.getPrice());
    }
}

测试结果:

巧克力奶茶
加了布丁!
加了椰果!
加了布丁的巧克力奶茶的价格:23.0

测试用例的两种写法结果都是一样的

总结

由上面的案例可以看出装饰器模式的代码结构,使用类的组合来代替继承,但其实装饰器模式相对于简单的组合关系,还有两个比较特殊的地方。

第一个比较特殊的地方是:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。 比如上面测试类中的第二种写法,对巧克力奶茶嵌套了两个装饰器类,给它添加了椰果和布丁。

第二个比较特殊的地方是:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。 实际上,符合“组合关系”这种代码结构的设计模式有很多,比如之前讲过的代理模式、桥接模式,还有现在的装饰器模式。尽管它们的代码结构很相似,但是每种设计模式的意图是不同的。就拿比较相似的代理模式和装饰器模式来说吧,代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。

在 Java 中的 IO 类库是使用装饰器模式的经典场景。

image.png

InputStream in = new FileImputStream("/user/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();