天天看点

Decorator(装饰器) 模式

12.1 Decorator 模式

  假如有一块蛋糕,如果只涂上奶油,其他什么也不加,就是奶油蛋糕。如果加上草莓,就是草莓奶油蛋糕。如果再加上一块黑色巧克力板,上面用白色巧克力写上姓名,然后插上蜡烛,就变成了一块生日蛋糕。

  不论是蛋糕、奶油蛋糕、草莓奶油蛋糕,他们的核心都是蛋糕。不过,经过一系列装饰后,蛋糕的味道变得更加甜美了,目的也变得更加明确了。

  程序中的对象与蛋糕十分相似。首先有一个相当于蛋糕的对象,然后不断地装饰蛋糕一样地不断地对齐增加功能,它就编程了使用目的更加明确的对象。

  像这样不断地为对象添加装饰的设计模式被称为 Decorator 模式。

12.2 示例程序

  示例程序的功能是给文字添加装饰边框。指用 “-”、“\”、“+”、“|” 组成的边框。

Decorator(装饰器) 模式

示例程序类图

|| Display 类

  Display 类是可以显示多行字符串的抽象类。

  getColumns 方法和 getRows 方法分别用于获取横向字符数和纵向行数。都是抽象方法需要子类实现。getRowText 方法用户获取指定的某一行的字符串。

  show 是显示所有行的字符串方法。在show 内部,程序会调用 getRows 方法获取行数,调用 getRowText 方法获取改行需要显示的字符串,然后循环显示字符串。属于 Template Method 模式。

public abstract class Display {

    public abstract int getColumns(); // 获取横向字符数

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

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

    public void show() { // 打印所有
        for (int i = 0; i < getRows(); i++) {
            System.out.println(getRowText(i));
        }
    }
}
           
|| StringDisplay 类

  StringDisplay 类是用于显示单行字符串的类。是 Display 的子类,因此肩负着实现 Display 类中声明的抽象方法的重任。

  string 字段中保存的是要显示的字符串。由于 StringDisplay 类只显示一行字符串,因此 getColumns 返回 string.getBytes().length 的值,getRows 方法返回固定值1。

  仅当要获取第 0 行时 getRowText 方法才会返回 string 字段。以本章开头的蛋糕的比喻来说, StringDisplay 类就相当于生日蛋糕中的核心蛋糕。

/**
* 显示核心类.
* 只显示一行字符串。
*/
public class StringDisplay extends Display{

    private String string;

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

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

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

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

  Border 类是装饰边框的抽象类。虽然它所表示的是装饰边框,但它也是 Display 的子类。

  也就是说,通过继承,装饰边框与被装饰物具有了相同的方法。具体而言,Border 类继承了父类的各方法。从接口角度而言,装饰边框与被装饰物具有相同的方法也就意味着它们具有一致性。

  在装饰边框 Border 类中有一个 Display 类型的 display 字段,它表示装饰物。不过,其所表示的装饰物并不仅限于 StringDisplay 的实例。因为 Border 也是 Display 的子类,display 字段也可以表示其他的装饰边框,而且那个边框中也有一个 display 字段。

/**
* 装饰边框的抽象类.
*/
public abstract class Border extends Display {

    protected Display display;

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

}
           
|| SideBorder 类

  SideBorder 类是一种具体的装饰边框,是 Border 类的子类。SideBorder 类是指定的字符装饰字符串的左右两侧。例如,字符为 ‘|’

|被装饰物|

  getColumns 方法是用于获取横向字符数的方法。只需要在被装饰物的字符数的基础上,再加上两侧边框的字符数即可。

1 + display.getColumns() + 1

  因为 SideBorder 类并不会在字符串的上下两侧添加字符,因此 getRows 方法直接返回 display.getRows() 即可。

  getRowText 方法可以获取参数指定的那一行的字符数,因此,像下面这样,在 display.getRowText(row) 的字符串两侧,加上 borderchar 这个装饰边框即可。

borderchar + display.getRowText(row) + borderchar

/**
* 具体的装饰器-在两端加上字符
*/
public class SideBorder extends Border {

    private char borderChar;

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

    @Override
    public int getColumns() {
        return 1 + display.getColumns() + 1; // 因为在两端加上字符,可以表示为 1 + 字符数 + 1
    }

    @Override
    public int getRows() {
        return display.getRows(); // 未改变 Row
    }

    @Override
    public String getRowText(int row) {
        return borderChar + display.getRowText(row) + borderChar;
    }
}
           
|| FullBorder 类

  FullBorder 类与 sideBorder 类相似。其会在字符串的上下左右都加上装饰边框。

/**
* 在字符串的上下左右都加上装饰边框.
* +-----+
* |Hello|
* +-----+
*/
public class FullBorder extends Border {

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

    @Override
    public int getColumns() {
        return 1 + display.getColumns() + 1;
    }

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

    @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 ch, int count) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < count; i++) {
            sb.append(ch);
        }
        return sb.toString();
    }
}
           
|| Main 类

  用于测试程序行为的类。b4 用于加多重边框。

public class Main {
    public static void main(String[] args) {
        Display b1 = new StringDisplay("Hello, world.");
        Display b2 = new SideBorder(b1, '#');
        Display b3 = new FullBorder(b2);
        b1.show();
        b2.show();
        b3.show();
        Display b4 = new SideBorder(
                new FullBorder(
                        new FullBorder(
                                new SideBorder(
                                        new FullBorder(
                                                new StringDisplay("Hello, World.")
                                        ), '*'
                                )
                        )
                ), '/'
        );
        b4.show();
    }
}
           

运行结果:

Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+
/+-------------------+/
/|+-----------------+|/
/||*+-------------+*||/
/||*|Hello, World.|*||/
/||*+-------------+*||/
/|+-----------------+|/
/+-------------------+/
           

12.3 Decorator 模式中登场角色

  在 Decorator 模式中有以下登场角色。

  ◆ Component

  增加功能时的核心角色。以开头场景为例,装饰前的蛋糕就是 Component 角色。Component 角色只是定义了蛋糕的接口(API)。在示例程序中,由 Display 类扮演此角色。

  ◆ ConcreteComponent

  该角色是实现了 Component 角色所定义的接口的具体蛋糕。在示例程序中,由 StringDisplay 类扮演此角色。

  ◆ Decorator(装饰物)

  该角色具有与 Component 角色相同的接口。在它内部保存了被装饰对象 - Component 角色。Decorator 角色知道自己要装饰的对象。在示例程序中,由 Border 类扮演此角色。

  ◆ ConcreteDecorator (具体的装饰物)

  该角色是具体的 Decorator 角色。在示例程序中,由 SideBorder 类和 FullBorder 类扮演此角色。

Decorator(装饰器) 模式

Decorator 模式的类图

12.4 拓展思路的要点

|| 接口(API)的透明性

  在 Decorator 模式中,装饰边框与被装饰物具有一致性。具体而言,在示例程序中,表示装饰边框的 Border 类是表示被装饰物的 Display 类的子类,这就体现了它们之间的一致性。也就是说 Border 类(及其子类)与表示被装饰物的 Display 类具有相同的接口。

  这样,即使被装饰物被边框装饰起来了,接口(API)也不会被隐藏起来。其他类依然可以调用 getColumns、getRows、getRowText 以及 show 方法。这就是接口(API)的透明性。

  得益于接口的透明性,Decorator 模式中也形成了类似于 Composite 模式中的递归结构。也就是说,装饰边框里面的 “被装饰物” 实际上又是别的物体的 “装饰边框” 。不过,Decorator 模式虽然与 Composite 模式一样,都具有递归结构,但是它们的使用目的不同。Decorator 模式主要的是通过添加装饰物来增加对象的功能。

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

  在 Decorator 模式中,装饰边框与被装饰物具有相同的接口(API)。虽然接口是相同的,但装饰越多,功能则越多。例如,用 SideBorder 装饰 Display 后,就可以在字符串的左右两侧加上装饰字符。如果再用 FullBorder 装饰,那么就可以在字符串的四周加上边框。此时,我们完全不需要对被装饰的类做任何修改。这样,我们就实现了不修改被装饰的类即可增加功能。

|| 可以动态的增加功能

  Decorator 模式中用到了委托,它使类之间形成了弱关联关系。因此,不用改变框架代码,就可以生成一个与其他对象具有不同关系的新对象。

|| 只需要一些装饰物即可添加许多功能

  使用 Decorator 模式可以为程序添加许多功能。只要准备一些装饰边框,即使这些装饰边框都只具有非常简单的功能,也可以将它们自由组合成为新的对象。

  就像我们可以自由选择香草味冰激凌、巧克力冰激凌、草莓味冰激凌一样。如果冰激凌店要为顾客准备冰激凌成品那就太麻烦了。因此,冰激凌店只会准备各种香料,当顾客下单后只需要在冰激凌上加上各种香料就可以了。Decorator 模式就是可以应对这种多功能对象的需求的一种模式。

|| java.io 包与 Decorator 模式

  java.io 包是用于输入输出的包。在其中,使用了 Decorator 模式。

  首先,可以像下面这样生成一个读取文件的实例。

Render render = new FileReader("data.txt");
           

  然后,像下面这样在读取文件时将文件内容放入缓冲区。

Reader reader = new BufferedReader(new FileReader("data.txt"));
           

  这样,在生成 BufferedReader 类的实例时,会指定将文件读取到 FileReader 类的实例中。

  在然后,我们可以想下面这样管理行号。

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

  可以看出,无论是 LineNumberReader 还是 BufferedReader 类的构造器,都可以接收 Reader 类的实例作为参数。

  除了 java.io 包以外,还在 javax.swing.border 包中使用了 Decorator 模式。其为我们提供了可为界面中的控件添加装饰边框的类。

|| 导致增加许多很小的类

  Decorator 模式的一个缺点就是会导致程序增加许多功能类似的很小的类。

12.6 延伸阅读:继承和委托中的一致性

  我们再稍微了解一下 “一致性”,即 “可以将不同的东西当作同一种东西看待” 的知识。

|| 继承-父类与子类的一致性

  子类和父类具有一致性。如:

class Parent {
    ...
    void parentMethod() {
        ...
    }
}

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

  此时,Child 类的实例可以保存在 Parent 类型的变量中,也可以调用从 Parent 类中继承的方法。也就是说,可以像操作 Parent 类的实例一样操作 Child 类的实例。这是将子类当作父类看待。

  但是,反过来,将父类当作子类一样操作,则需要先进行类型转换(必须是子类的对象)。

Parent obj = new Child();
((Child)obj).childMethod();
           
|| 委托-自己和被委托对象的一致性

  使用委托让接口具有透明性,自己和被委托对象具有一致性。

  下面我们来看一个稍微有点生硬的例子。

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

class Violet {
    void method() {
        ...
    }
}
           

  Rose 和 Violet 都有相同的 method 方法。Rose 将 method 方法的处理委托给了 Violet。这样会让人有一种好像这两个类有所关联,又好像没有关联的感觉。

  因为这两个类虽然都有 method 方法,但是却没有明确地在代码中提现出这个 “共通性”。如果要明确地表示 method 方法是共通的,只需要像下面这样编写一个共通的抽象类 Flower 就可以了。

abstract class Flower {
    abstract void method();
}

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

class Violet extends Flower {
    void method() {
        ...
    }
}
           

  或是将 Flower 作为接口也行。

interface Flower {
    void method();
}
           

  至此,可能会产生这样的疑问,即 Rose 类中的 Obj 字段被指定为具体的 Violet 真的好吗?如果指定为抽象类型 Flower 会不会更好呢?究竟应该怎么做才好,其实没有固定的答案,需求不同,做法也会不同。

继续阅读