品设计模式 - (结构型) 装饰器模式 Decorator

202 阅读11分钟
这里写图片替代文字

简介

目标: 为对象动态添加功能;

定义:动态地给一个对象添加额外的职责。就增加功能来说,装饰模式比生成子类更为灵活

而继承实际上是不灵活的复用方式。

具体步骤: 为与原使用被装饰对象的代码无缝结合,定义抽象类实现与被装饰对象相同接口,在具体实现类中转调被装饰对象,于转调前后添加新功能,实现为被装饰对象增功能,思路与“对象组合”类似。

本质

功能细化,动态组合

适用场景

  1. 需要在不影响其他对象的情况下,以动态、透明的方式给对象添加职责
  2. 不适合适用子类来进行扩展的时候

入门案例一:打印带有装饰边框的 Hello World!

输出示例

+--------------+
|Hello, world. |
+--------------+

设计类

类名简介
Display用于显式字符串的抽象类
StringDisplay显式单行字符串的类
Border显式装饰边框的抽象类
SideBorder只显式左右边框的类
FullBorder显式上下左右边框的类
Main测试程序

类图设计

画板

Display 类

可以显式多行字符串的抽象类

  • getColumns(): 抽象方法,获取横向字符数
  • getRows(): 抽象方法,获取纵向行数
  • getRowText(): 抽象方法,获取指定的某一行字符串
  • show(): 显式所有行的字符串的方法,内部调用 getRows 获取行数和 getRowText 获取该行需要显示的字符串,然后通过 for 循环语句将所有的字符串显示出来。
public abstract class Display {

    public abstract int getColumns();   // 获取列数

    public abstract int getRows();  // 获取行数

    public abstract String getRowText(int row); //获取第 row 行的字符串

    //输出显示
    public final void show() {
        for (int i = 0; i < getRows(); i++) {
            System.out.println(getRowText(i));
        }
    }
}

StringDisplay 类

用于显示单行字符串的类。它为 Display 的子类,需要实现 Display 类中声明的所有抽象方法的重任。

其中:

  1. string 字段中保存的是要显示的字符串。
  2. 由于 StringDisplay 类只显示一行字符串。所以 getColumns() 直接返回 string.getBytes().length 的值。
  3. getRows 方法则返回固定值 1
  4. getRowText 仅当获取第 0 行的内容的时候才会返回 string 字段
public class StringDisplay extends Display {

    private String string;

    public StringDisplay(String string) {
        this.string = string;
    }

    @Override
    public int getColumns() {
        return this.string.getBytes().length;
    }

    @Override
    public int getRows() {
        return 1;
    }

    @Override
    public String getRowText(int row) {
        if (row == 0) {
            return string;
        } else {
            return null;
        }
    }
}

Border 类

装饰边框的抽象类;但是它也是 Display 的子类。

因此,通过继承;装饰边框和被装饰物都具有了相同的方法。装饰边框 Border 与被装饰物 Display 具有相同的方法也就意味着它们具有一致性

  • Display 类型的 display 字段表示被装饰物
  • display 可能使 StringDiplay 实例又或者是 Border
public abstract class Border extends Display {

    protected Display display;

    protected Border(Display display) {
        this.display = display;
    }
}

SideBorder 类

一个具体装饰边框,是 Border 类的子类。SideBorder 类用指定的字符 borderchar装饰左右两侧;它实现了父类中声明的所有抽象方法。起到了装饰作用

  • getColumns: 获取横向字符数的方法。在被装饰物的字符数的基础上,再加上两侧边框的字符数即可。需要调用 display.getColumns() 即可得到被装饰物的字符数。
  • getRows: 因为 SideBorder 类并不会在字符串的上下两侧添加字符,因此 getRows 方法直接返回 display.getRows() 即可
  • getRowText: 获取参数指定的那一行字符串。即 bordeChar + display.getRowText() + borderChar 即可
public class SideBorder extends Border {

    private char borderChar;

    protected SideBorder(Display display, char c) {
        super(display);
        this.borderChar = c;
    }

    // 获取列数,原字符 + 左右边框字符
    @Override
    public int getColumns() {
        return display.getColumns() + 2;
    }

    @Override
    public int getRows() {
        return display.getRows();
    }

    // 获取当前行的内容
    @Override
    public String getRowText(int row) {
        return borderChar + display.getRowText(row) + borderChar;
    }
}

FullBorder 类

与 SideBorder 类一样,也是 Border 类的子类。作用区别:

  • SideBorder 类会在字符串的左右两侧加上装饰边框,
  • FullBorder 类则会在字符串的上下左右都加上装饰边框。

但是 SideBorder 可以指定边框,而 FullBorder 内的边框是固定的

新增 makeLine() 方法用于连续显示某一字段

public class FullBorder extends Border {

    protected FullBorder(Display display) {
        super(display);
    }

    @Override
    public int getColumns() {       // 列数(字符数) 为被装饰物加上两侧边框字符数
        return this.display.getColumns() + 2;
    }

    @Override
    public int getRows() {          // 行数为内容行 + 上下边框
        return this.display.getRows() + 2;
    }

    @Override
    public String getRowText(int row) {         // 指定获取哪一行的字符串
        if (row == 0) {                             // 下边框
            return "+" + makeLine('-', display.getColumns()) + "+";
        } else if (row == display.getRows() + 1) {  // 上边框
            return "+" + makeLine('-', display.getColumns()) + "+";
        } else {                                    // 其他边框
            return "|" + display.getRowText(row - 1) + "|";
        }
    }

    private String makeLine(char c, int count) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count; i++) {
            sb.append(c);
        }
        return sb.toString();
    }
}

Main 类

测试程序行为的类。生成 4 个实例,即 b1 ~ b4。作用:

  • b1: 将 "Hello, world." 不加装饰地直接显示出来
  • b2: 将 b1 的两侧加上装饰边框 '#'
  • b3: 将 b2 的上下左右加上装饰边框
  • b4: 为 "你好 世界" 加上多重边框
public class Main {

    public static void main(String[] args) {

        StringDisplay b1 = new StringDisplay("Hello, world.");      // 指定输出内容
        SideBorder b2 = new SideBorder(b1, ' ');                 // 添加左右边框
        FullBorder b3 = new FullBorder(b2);                         // 输出携带上下边框 + 左右边框的
        b1.show();
        b2.show();
        b3.show();

        /*
         输出:
            Hello, world.
             Hello, world.
            +---------------+
            | Hello, world. |
            +---------------+
        */

        // 嵌套实例
        SideBorder b4 = new SideBorder(
                new FullBorder(
                        new FullBorder(
                                new SideBorder(
                                        new FullBorder(
                                                new StringDisplay("Hello! Jools Wakooo!!!")),
                                        '*')
                        )
                ), '/');

        b4.show();
        
        /*
         输出:
            /+----------------------------+/
            /|+--------------------------+|/
            /||*+----------------------+*||/
            /||*|Hello! Jools Wakooo!!!|*||/
            /||*+----------------------+*||/
            /|+--------------------------+|/
            /+----------------------------+/
        */
    }
}

其中 b1, b2, b3 的关系

画板

Decorator 模式中的核心组件

UML

画板

核心组件

角色简介
Component增加功能的核心角色, 由示例中的 Display 扮演
ConcreteComponent实现了 Component 角色所定义的接口,如 **StringDisplay **
Decorator具有与 Component 角色相同的接口,内部保存了被装饰对象 —— Component 角色,知道自己要装饰的对象,示例中由 Border 角色充当
ConcreteDecorator具体的 Decorator 角色,由示例中的 SideBorder 和 FullBorder 充当

接口 API 的透明性

⌈装饰边框⌋⌈被装饰物⌋ 需要具有 一致性。比如示例中,⌈装饰边框⌋ Border 类是表示 ⌈被装饰物⌋ Display 类的子类。Border 类及其子类与表示 ⌈被装饰物⌋ 的 Display 类具有相同的接口接口不会被隐藏起来。其他类依然可以调用如 getColumnsgetRows 以及 show 方法。

不改变被装饰物的前提下增加功能

⌈装饰边框⌋⌈被装饰物⌋ 具有相同的接口(抽象类),但是虽然接口相同,装饰越多,功能则越多。使用了委托,对 ⌈装饰边框⌋ 提出的要求会被转交给 ⌈被装饰物⌋ 去处理。

Java.io 包与 Decorator 模式

读取文件实例

Reader reader = new FileReader("xxxx.txt")

读取文件的时候将内容加载入缓冲区

Reader reader = new BuffredReader(
    new FileReader("default.txt")
);

添加行号管理

Reader reader = new LineNumberReader(
                    new BuffredReader(
                        new FileReader("default.txt")
                    )
                );

并且无论是 LineNumberReader 类的构造函数还是 BufferedReader 类的构造函数。都可以接收 Reader 类(的子类)的实例作为参数。可以自由组合

比如:只管理行号而不进行缓存处理

Reader reader = new LineNumberReader(
    new FileReader("default.txt")
);

也可以不从文件中读取数据,而是从网络中读取数据

Reader reader = new LineNumberReader(
        new BufferedReader(
            new InputStreamReader(
                socket.getInputStream();
            )
        )
    );

这里使用 InputStreamReader 类既接收 getInputStream 方法返回的 InputStream类的实例作为构造函数的参数,也提供了 Reader 类的接口 API

什么是一致性?

继承 —— 父类与子类的一致性

class Parent {
    ...

    void parentMethod() {
        ...
    }
}
class Child extends Parent {
    ...
    void childMethod() {
        ...
    }
}

子类实例支持直接调用父类方法

Parent obj = new Child();
obj.parentMethod();

也就是说像操作 Parent 类的实例一样操作 Child 实例

但是如果反过来,需要先进行类型转换

Parent obj = new Child();

((Child) obj).childMetod();

委托 —— 自己和被委托对象的一致性

比如

class Rose {
    
    Violet obj = ....;

    void method() {
        obj.method();
    }
}
Class Violet {
    void method() {
        ...
    }
}

两者具有相同的 method 方法。而 Rose 将 method 方法的处理委托给 Violet。

或者基于抽象类或者接口表明其为共通关系

abstract class Flower {
    abstract void method();
}
class Rose extends Flower {
    Violet obj = ...;
    void method() {
        obj.method();
    }
}
class Violet extends Flower {
    void method() {
        ....
    }
}

接口实现同理

入门案例一:练习

[练习] - 扩展入门案例一,添加 UpDownBorder类,支持为字符串装饰上下两条边框

输出示例

  • 当设置上界字符为 - 的时候
Hello,world.

-----------
Hello,world.
-----------
  • 叠加装饰器
Hello!! World!!!

----------------
Hello!! World!!!
----------------

*----------------*
*Hello!! World!!!*
*----------------*

实现新的具体装饰器 ConcreteDecorator - UpDownBorder

public class UpDownBorder extends Border {

    private char upDownChar;
    private String bound;

    public UpDownBorder(Display display, char upDownChar) {
        super(display);
        this.upDownChar = upDownChar;
        this.bound = makeBorder();
    }

    @Override
    public int getColumns() {
        return display.getColumns();    //每一行字符数不变
    }

    @Override
    public int getRows() {
        return display.getRows() + 2;   //新增上下边界
    }

    //重构当前行内容,添加上下边界
    @Override
    public String getRowText(int row) {
        if (row == 0) {                             // 下边框
            return bound;
        } else if (row == display.getRows() + 1) {  // 上边框
            return bound;
        } else {                                    // 其他边框
            return display.getRowText(row - 1);
        }
    }

    // 构建上下边界
    private String makeBorder() {
        int cols = getColumns();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < cols; i++) {
            sb.append(upDownChar);
        }
        return sb.toString();
    }
}

测试

    @Test
    public void showUpDownBound() {

        StringDisplay b1 = new StringDisplay("Hello!! World!!!");    // 基础
        UpDownBorder b2 = new UpDownBorder(b1, '-');     // 添加上下边框
        SideBorder b3 = new SideBorder(b2, '*');                  // 添加侧边框
        FullBorder b4 = new FullBorder(b3);                          // 添加全边框

        b1.show();

        System.out.println();
        b2.show();

        System.out.println();
        b3.show();

        System.out.println();
        b4.show();

        SideBorder b5 = new SideBorder(b3, '|');    // 新增侧边框
        UpDownBorder b6 = new UpDownBorder(b5, '/');    // 新增上下边框

        System.out.println();
        b5.show();

        System.out.println();
        b6.show();
        
        /*
         输出:
         Hello!! World!!!

        ----------------
        Hello!! World!!!
        ----------------
        
        *----------------*
        *Hello!! World!!!*
        *----------------*
        
        +------------------+
        |*----------------*|
        |*Hello!! World!!!*|
        |*----------------*|
        +------------------+
        
        |*----------------*|
        |*Hello!! World!!!*|
        |*----------------*|
        
        ////////////////////
        |*----------------*|
        |*Hello!! World!!!*|
        |*----------------*|
        //////////////////// 
        */

    }

[练习] - 新增可以显示多行字符串 MultiStringDisplay 类,在 Decorator 模式中扮演 ConcreteComponent 角色

示例、

MultiStringDisplay md = new MultiStringDisplay();
md.add("早上好。");
md.add("下午好。");
md.add("晚安,明天见。");
md.show();
/*
 输出结果: 
 早上好。
 下午好。
 晚安,明天见。
*/

Display d1 = new SideBorder(md, '#');
d1.show();

/*
 输出结果: 
 #早上好。	   # 
 #下午好。     #  
 #晚安,明天见。#
*/


Display d2 = new FullBorder(md);
d2.show();
/*
 输出结果: 
 +-------------+
 |早上好。      |
 |下午好。      |
 |晚安,明天见。 |
 +-------------+
*/

代码实现

public class MultiStringDisplay extends Display {

    private List<String> strs;

    public MultiStringDisplay() {
        this.strs = new ArrayList<>();
    }

    public void add(String s) {
        this.strs.add(s);
    }

    @Override
    public int getColumns() {
        int maxSize = Integer.MIN_VALUE;
        for (String str : strs) {
            maxSize = Math.max(str.length(), maxSize);
        }
        return maxSize;
    }

    @Override
    public int getRows() {
        return this.strs.size();
    }

    @Override
    public String getRowText(int row) {
        int size = getColumns();
        String str = strs.get(row);
        return str + " ".repeat(Math.max(0, size - str.length())); //
    }
}

测试

    @Test
    public void testMultiStrs() {

        MultiStringDisplay md = new MultiStringDisplay();
        md.add("Good Morning");
        md.add("Good Afternoon");
        md.add("See you soon!!!");
        md.show();

        System.out.println();
        SideBorder b1 = new SideBorder(md, '*');
        b1.show();

        System.out.println();

        FullBorder b2 = new FullBorder(b1);
        b2.show();
        
        /*
         输出:
            Good Morning   
            Good Afternoon 
            See you soon!!!
            
            *Good Morning   *
            *Good Afternoon *
            *See you soon!!!*
            
            +-----------------+
            |*Good Morning   *|
            |*Good Afternoon *|
            |*See you soon!!!*|
            +-----------------+ 
        */
    }

入门案例二:饮料添加配料

题目描述

不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,要求计算一种饮料的价格

下图表示在深度烘焙咖啡(DarkRoast)饮料上新增添加摩卡(Mocha)配料,之后又添加了鲜奶油(Whip)配料。

深度烘焙咖啡被摩卡包裹,摩卡又被鲜奶油包裹。

它们都继承自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。

设计

image.png

Component - Beverage

public interface Beverage {

    double cost();
}

ConcreteComponent - DarkRoast

public class DarkRoast implements Beverage{
    @Override
    public double cost() {
        return 1;
    }
}

Decorator - CondimentDecorator

public abstract class CondimentDecorator implements Beverage {

    protected Beverage beverage;
}

ConcreteDecorator - Milke + Mocha

public class Milk extends CondimentDecorator{

    public Milk(Beverage beverage) {
        this.beverage = beverage;
    }

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

public class Mocha extends CondimentDecorator {

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

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

测试

public class Client {

    public static void main(String[] args) {

        DarkRoast beverage = new DarkRoast();
        System.out.println(beverage.cost());

        Beverage b2 = new Milk(beverage);
        System.out.println(b2.cost());

        Beverage b3 = new Mocha(new Milk(beverage));
        System.out.println(b3.cost());
        
        /*
         输出:
            1.0
            2.0
            3.0
        */
    }
}

装饰模式的优缺点

优点

  1. 比继承更加灵活: 继承是静态的,所有子类共享相同的功能。而装饰模式通过将功能分离到各个装饰器中,并在运行时动态组合这些装饰器,从而实现灵活的功能组合。最终的功能由运行时动态组合的装饰器决定。
  2. 更加容易复用功能: 把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器仅实现一个功能。

缺点

  1. 产生很多细粒度对象: 为与原使用被装饰对象的代码无缝结合,定义抽象类实现与被装饰对象相同接口,在具体实现类中转调被装饰对象,于转调前后添加新功能,实现为被装饰对象增功能,思路与“对象组合”类似。

巩固案例 - 咖啡加糖

原题来源

卡码网 - KamaCoder 设计模式之装饰器模式

题目描述

小明喜欢品尝不同口味的咖啡,他发现每种咖啡都可以加入不同的调料,比如牛奶、糖和巧克力。他决定使用装饰者模式制作自己喜欢的咖啡。

请设计一个简单的咖啡制作系统,使用装饰者模式为咖啡添加不同的调料。系统支持两种咖啡类型:黑咖啡(Black Coffee)和拿铁(Latte)。

输入输出示例

UML设计

实现 Component (Coffee 接口)

// Component
interface Coffee {
    
    void brew();
}

实现 ConcreteComponent

// ConcreteComponent 01
class BlackCoffee implements Coffee {
    
    @Override
    public void brew() {
        System.out.println("Brewing Black Coffee");
    }
}

// ConcreteComponent 02
class Latte implements Coffee {
    
    @Override
    public void brew() {
        System.out.println("Brewing Latte");
    }
}

实现 Decorator 抽象类

  • 注意:持有的 Coffee 字段可以用 protected修饰简化代码; 支持子类直接访问
abstract class CondimentDecorator implements Coffee {
    
    // 对子类可见
    protected Coffee coffee;
    
    public CondimentDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
}

实现 ConcreteDecorator

class Milk extends CondimentDecorator {
    
    public Milk(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public void brew(){
        super.coffee.brew();
        System.out.println("Adding Milk");
    }
}

class Sugar extends CondimentDecorator {
    
    public Sugar(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public void brew() {
        super.coffee.brew();
        System.out.println("Adding Sugar");
    }
}

执行结果

参考