设计模式(3)-分开考虑与一致性

Bridge

简介

Birdge模式的功能是完成类的功能层次结构和实现层次结构的连接。在介绍之前,我们首先需要分别介绍什么是类的功能层次结构,什么是类的实现层次结构。

对于一个类来说,我们可能想要基于它新增一些功能,这样实际上就构成了类的功能层次结构。在父类中具有基本功能,在子类中增加新的功能。例如我们可以得到如下的功能层次结构:

1
2
3
Something
+- SomethingGood
+- SomethingBetter

另一方面,对于抽象类来说,子类需要对其中的抽象方法进行实现。此时也会形成一种层次结果,实际上就是类的实现层次结构。

1
2
3
AbstractClass
+- ConcreteClass
+- AnotherConcreteClass

由于这两种层次结构的存在,我们在编写子类的时候,首先应该确认自己的意图,是需要新增功能还是需要新增实现。当类的层次结构只有一层的时候,功能结构层次与实现结构层次混杂在一起,这样很容易使得类的层次结构变得复杂,也难以透彻理解类的层次结构。而Bridge模式就是用于这两种层次结构的分离。

示例程序

考虑我们现在有一个顶级的抽象类Display,它的核心方法是display,它需要达到的效果是按照顺序分别调用三个抽象方法,分别是open、print和close。一方面,由于Display是抽象类,因此我们还需要有具体的实现StringDisplay。另一方面,我们会需要拓展Display的功能,让它能够提供循环输出的能力,因此会有一个CountDisplay来进行功能扩展。

如果不使用Bridge模式,我们可能会得到如下的程序。首先定义顶级抽象类Display,其中分别声明三个方法open、print和close,同时实现核心方法display。

1
2
3
4
5
6
7
8
9
10
public abstract class Display {
public abstract void open();
public abstract void print();
public abstract void close();
public void display(){
open();
print();
close();
}
}

之后,完成具体的实现StringDisplay

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StringDisplay extends Display {
private String string;

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

@Override
public void open() {
System.out.println("string display open");
}

@Override
public void print() {
System.out.println("< " + string + " >");
}

@Override
public void close() {
System.out.println("string display close");
}
}

然后基于这个实现,我们可以进一步使用继承来扩展Display的功能,即完成CountDisplay

1
2
3
4
5
6
7
8
9
10
11
public class CountDisplay extends StringDisplay {
public CountDisplay(String string) {
super(string);
}

public void multiDisplay(int times) {
for (int i = 0; i < times; i++) {
print();
}
}
}

虽然上面这种实现的确能够达到效果,但是从类的层次结构来看,却可能存在一些问题。我们可以看到它们的继承关系如下:

对于抽象类Display来说,它的实现层次结构里包含了StringDisplay,实现的确是基于Display的。可是对于它的功能层次结构,CountDisplay却是基于了StringDisplay,这就导致了类的实现结构层次与功能结构层次没有分离。在相关逻辑较为简单的时候,可能还能够管理,如果逻辑更加复杂,那么理解起来就会比较混乱了。

那么下面我们来看看Bridge模式如何达到实现结构层次和功能结构层次的分离。

首先同样需要定义顶级类Display。但是与上面不同的是,Display本身并不是一个抽象类,但是它持有一个抽象类DisplayImpl。其中对应的方法则是调用这个抽象类的相关方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Display {
private DisplayImpl displayImpl;

public Display(DisplayImpl displayImpl) {
this.displayImpl = displayImpl;
}

public void open() {
this.displayImpl.rawOpen();
}

public void print() {
this.displayImpl.rawPrint();
}

public void close() {
this.displayImpl.rawClose();
}

public final void display() {
open();
print();
close();
}
}

从名字上来看,DisplayImpl应该是Display的实现类,但是这里它是一个抽象类,它代表的逻辑含义是所有的Display的实现类都应该来实现DisplayImpl,或者说该类可以理解为Display的抽象实现类,当然可能有点拗口。不过就是这个抽象的DisplayImpl,实现了类功能层次结构与实现层次结构的分离,同时起到birdge的效果。

1
2
3
4
5
public abstract class DisplayImpl {
public abstract void rawOpen();
public abstract void rawPrint();
public abstract void rawClose();
}

接下来,对于真正的实现StringDisplay,它需要继承的是DisplayImpl,并实现其中相关的抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StringDisplay extends DisplayImpl {
private String string;

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

@Override
public void rawOpen() {
System.out.println("string display open");
}

@Override
public void rawPrint() {
System.out.println("< " + string + " >");
}

@Override
public void rawClose() {
System.out.println("string display close");
}
}

而对于功能扩展CountDisplay,它继承的则是Display,然后完成对应的功能扩展。

1
2
3
4
5
6
7
8
9
10
11
public class CountDisplay extends Display{
public CountDisplay(DisplayImpl displayImpl) {
super(displayImpl);
}

public void multiDisplay(int times) {
for (int i = 0; i < times; i++) {
print();
}
}
}

之后就可以完成测试代码了。在使用Display的时候,我们还需要传入对应的实现类,表示当前的Display需要使用那一个DisplayImpl:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test() {
Display display1 = new Display(new StringDisplay("Hello string display"));
Display display2 = new CountDisplay(new StringDisplay("Hello Count Display"));
CountDisplay display3 = new CountDisplay(new StringDisplay("Hello Count Display"));

display1.display();
display2.display();
display3.display();
display3.multiDisplay(3);
}

对比之前没有使用Bridge的实现,Bridge模式完成了类功能结构层次与实现结构层次的分离,就像示例代码中类之间的继承关系一样。

说明

从上面的示例代码中我们可以看到,Bridge模式的核心就在于DisplayImpl的引入。顶级类Display并不是自己去定义抽象方法,而是委托DisplayImpl去完成这项工作。相比于继承,委托是更加弱的关联关系。因为只有当对应的实例生成并赋值给委托对象之后,才会建立关联,因此很容易地改变当前需要使用的实现类。

而利用Bridge模式,我们实现了“类的功能层次结构”与“类的实现层次结构”分离,分离之后更加有利于独立地对它们进行扩展。当想要增加功能的时候,只需要在功能层次结构一侧增加类即可,而不必对实现层次结构做任何修改。而功能的扩展不再是基于某个特定的实现类了,而是所有的实现类都可以使用,只需要在初始化的时候传入对应的实现类就行。

Strategy

无论什么程序,它的目的都是用来解决问题的,在程序中我们通过编写特定的算法来实现问题的解决。Strategy指的就是这个算法,或者称为解决问题的策略。利用Strategy模式,可以实现整体地替换算法的实现部分。整体替换算法,使得我们能够轻松地以不同的方法去解决同一个问题,这就是Strategy模式。

在Strategy模式中,关键点在于确定Strategy接口。在这个接口中会声明抽象策略方法所需要的参数类型以及返回值类型,其他相关的实现类则可以实现对应的方法。而在使用的时候,则直接使用接口方法即可,而接口具体对应的是哪一个实现类,则进行简单替换即可。由于Strategy模式代码的逻辑结构相对简单,这里就不给出代码示例了。

Strategy模式将程序中的算法与其他部分分离开来,只定义了与算法相关的接口,然后在程序中以委托的方式来实现算法。使用这种弱关联关系可以很方便地整体替换算法,并且借助委托,算法的替换特别是动态替换成为了可能。

Composite

简介

在计算机中有目录的概念,而在目录中可以存放文件,也可以存放目录。我们可以将目录理解为一种容器,在容器中又可以存放文件或者目录。在这种层面上来说,目录形成了一种容器结构,同时这种容器具有递归结构。文件和目录都可以被存放在目录中,从这个角度来看,文件和目录被统称为目录条目Directory Entry,在目录条目中,文件夹和文件被当作是同一种对象,即它们具有一致性。

如果扩大视角,从容器层面来看,则表达的意思是在容器中既可以放入内容,也可以放入小容器,而在小容器中同样可以继续放入内容或者容器,形成递归结构的容器。Composite的意思是混合物,复合物。Composite模式能够完成的就是创建出这种递归结构的容器,在其中容器和内容具有一致性。

在Composite中主要三类角色:

  • Content:表示内容的角色。在该角色中不能放入其他对象
  • Composite:表示容器的角色,在其中可以放入Content角色或者Composite角色
  • Component:该角色用于统一Content和Composite角色,使这两个角色具有一致性。通常来说Component角色是Content和Composite角色的父类

示例程序

下面我们使用一个示例程序来表示上面目录的层级结构。首先可以完成目录和文件的统一抽象父类Entry。实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Entry {
public abstract String getName();

public abstract int getSize();

public Entry add(Entry entry) throws FileTreatmentException {
throw new FileTreatmentException("you can't add content into it");
}

protected abstract void printList(String prefix);

public void printList() {
printList("");
}

@Override
public String toString() {
return getName() + "(" + getSize() + ")";
}
}

在这个类中首先声明了两个抽象方法,分别用于获取名称以及大小,然后定义了一个add添加方法,用于向目录中增加新的元素。这个方法有默认实现,默认为抛出异常,这也就意味着只有重写了add方法的子类才能正常调用add添加方法。然后完成了输出方法,用于按照路径格式输出目录或者文件的路径。这里默认抛出的异常FileTreatmentException简单继承了RuntimeException

之后可以分别实现Entry的子类,文件File以及目录Directory。下面是文件类File的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class File extends Entry {
private String filename;
private int size;

public File(String filename, int size) {
this.filename = filename;
this.size = size;
}

@Override
public String getName() {
return filename;
}

@Override
public int getSize() {
return size;
}

@Override
protected void printList(String prefix) {
System.out.println(prefix + "/" + filename);
}
}

在文件类中,只需要在类中保存文件名称以及大小,然后简单实现对应的方法即可。而目录类则相对复杂一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Directory extends Entry {
private String directoryName;
private ArrayList<Entry> directory = new ArrayList<>();

public Directory(String name) {
this.directoryName = name;
}

@Override
public String getName() {
return directoryName;
}

@Override
public int getSize() {
int size = 0;
for (Entry entry : directory) {
size += entry.getSize();
}
return size;
}

@Override
protected void printList(String prefix) {
System.out.println(prefix + "/" + this);
for (Entry entry : directory) {
entry.printList(prefix + "/" + directoryName);
}
}

@Override
public Entry add(Entry entry) throws FileTreatmentException {
directory.add(entry);
return this;
}
}

在目录类中需要定义目录名称,同时需要定义一个容器来存放Entry对象。但是并不需要定义大小,因为目录的大小实际上是根据其中保存的Entry对象容器计算出来的。在目录类中需要实现add方法,其中的实现就是向容器中添加传入的Entry对象。这里的printList方法实现,也是遍历容器然后进行输出。由于这里的Entry可能是File也可能是Directory,所以在输出的时候会有递归的效果。

说明

在日常编程中,有很多地方都存在递归结构,而利用Composite模式就可以完成容器和内容的统一,在使用的时候则会更加方便。

Decorator

简介

Decorator意为装饰者模式。假设程序中的对象具有基本的功能,而利用装饰者模式,我们就可以不断地为其增加功能,同时不会掩盖初始的功能API,这就是装饰者模式。

在装饰者模式中,主要有如下角色:

  • Component:指的是被增加功能的核心角色。一般是抽象类,其中声明了核心功能API
  • ConcreteComponent:指的是实现了Component的具体角色,具有基本的功能
  • Decorator:指的是装饰者角色,该角色与Component角色具有相同的接口,保证功能API不被覆盖。同时该角色内部还保存一个Component对象,作为被装饰的对象
  • ConcretDecorator:指的是具体的装饰者,需要实现Decorator

装饰者模式的结构示意图如下:

示例程序

下面我们就利用装饰者模式来完成一个示例程序,这个程序的功能是给文字字符串添加装饰边框。

首先我们完成一个抽象类Display,这个抽象类可以显示多行字符串,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Display {
public abstract int getColumns();

public abstract int getRows();

public abstract String getRowText(int row);

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

在Display抽象类中有三个抽象方法,分别用于获取需要打印的字符串的行数和列数,以及根据给定的行数row来获取对应行的信息。之后利用这些抽象方法来完成show方法,用于字符串的显示。单独看这个抽象类可能不容易理解,那么下面我们就通过继承这个抽象类,完成一个实现子类StringDisplay。这个子类可以打印一行字符串,具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class StringDisplay extends Display {
private final 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;
} else {
return null;
}
}
}

这里需要注意的是getRowText方法只能接收row=0的情况,其他情况都会返回null。至此,基本的功能实现类已经有了,就是StringDisplay,下面我们需要对其进行装饰,完成功能的扩展。而在这里,功能的扩展指的就是给字符串添加显示边框。

首先我们需要完成一个Border类,它是后续相关装饰功能类的统一父类。需要注意的是,Border类是Display的子类,同时自身也还是一个抽象类。通过继承,装饰边框与被装饰物具有了相同的方法。而在这个Border类中,又持有了一个Display对象。

1
2
3
4
5
6
7
public abstract class Border extends Display {
protected Display display;

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

那么接下来就可以来实现Border类的子类了,这里分别实现了SideBorder类和FullBorder类。SideBorder类可以使用指定的字符来装饰字符串的左右两侧,而FullBorder类则使用固定的边框来框住字符串的上下左右,它们的实现分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SideBorder extends Border {
private char borderChar;

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

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

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

@Override
public String getRowText(int row) {
return borderChar + display.getRowText(row) + borderChar;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 || row == getRows() - 1) {
return "+" + makeLine('-', display.getColumns()) + "+";
} else {
return "|" + display.getRowText(row - 1) + "|";
}
}

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

接下来就可以完成测试代码了,尤其需要体会下面display4对象的构造过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test() {
Display display1 = new StringDisplay("Hello,world");
Display display2 = new SideBorder(display1, '#');
Display display3 = new FullBorder(display2);
Display display4 =
new SideBorder(
new FullBorder(
new FullBorder(
new SideBorder(
new FullBorder(
new StringDisplay("Hello,world")
),'*'))),'/');
display1.show();
display2.show();
display3.show();
display4.show();
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
Hello,world
#Hello,world#
+-------------+
|#Hello,world#|
+-------------+
/+-----------------+/
/|+---------------+|/
/||*+-----------+*||/
/||*|Hello,world|*||/
/||*+-----------+*||/
/|+---------------+|/
/+-----------------+/

说明

在Decorator模式中,装饰者与被装饰者具有一致性。它们的接口都是统一的,这是因为装饰者抽象类继承了被装饰者抽象类,保证了接口的统一,同时装饰者通过持有被装饰者,来完成功能的扩展。这样即使被装饰者进行了多次的装饰,原始核心的功能API也不会被隐藏起来。得益于API的透明性,Decorator模式中也形成了类似于Composite模式中的递归结构。换句话说,装饰边框里面的“被装饰物”实际上又是别的物体的“装饰边框”。

Decorator模式主要用于功能的增加,通过该模式,我们可以实现不修改被装饰的类即可增加功能。它使用委托,使得类之间形成弱关联关系。但是Decorator模式本身还是有一定的缺点,就是会导致在程序中增加许多功能类似但是很小的类。实际上,在java.io包中,有很多用于IO的类就是使用了装饰者模式。


设计模式(3)-分开考虑与一致性
http://example.com/2023/03/23/设计模式-3-分开考虑与一致性/
作者
EverNorif
发布于
2023年3月23日
许可协议