Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.(允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。)

代码场景

现在要开发一个工具来处理 PDF、PPT、Word 资源文件。这个工具的其中一个功能是,把这些资源文件中的文本内容抽取出来放到 txt 文件中。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 抽象类
public abstract class ResourceFile {
protected String filePath;

public ResourceFile(String filePath) {
this.filePath = filePath;
}

public abstract void extract2txt();
}

public class PPTFile extends ResourceFile {
public PPTFile(String filePath) {
super(filePath);
}

@Override
public void extract2txt() {
//...省略一大坨从PPT中抽取文本的代码...
//...将抽取出来的文本保存在跟filePath同名的.txt文件中...
System.out.println("Extract PPT.");
}
}

public class PdfFile extends ResourceFile {
public PdfFile(String filePath) {
super(filePath);
}

@Override
public void extract2txt() {
//...
System.out.println("Extract PDF.");
}
}
// WordFile ...

public class ToolApplication {
public static void main(String[] args) {
List<ResourceFile> resourceFiles = listAllResourceFiles();
for (ResourceFile resourceFile : resourceFiles) {
resourceFile.extract2txt();
}
}

private static List<ResourceFile> listAllResourceFiles() {
List<ResourceFile> resourceFiles = new ArrayList<>();
//...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
resourceFiles.add(new PdfFile("a.pdf"));
resourceFiles.add(new WordFile("b.word"));
resourceFiles.add(new PPTFile("c.ppt"));
return resourceFiles;
}
}

成功完成需求!(技术文专业转折点!)

这时候产品要求可以支持压缩!不就在 ResourceFile 再写个抽象方法,所有子类重写实现以下呗 ~,那再加一个功能呢?( ̄へ ̄,你说加就加?那我岂不是很没面子?)

如果继续按照上面的实现思路,就会存在这样几个问题:

  • 违背开闭原则,添加一个新的功能,所有类的代码都要修改;
  • 功能增多,每个类的代码都不断膨胀,可读性和可维护性都变差了;
  • 把所有比较上层的业务逻辑都耦合到 PdfFile、PPTFile、WordFile 类中,导致这些类的职责不够单一,变成了大杂烩。

初步优化

针对上面的问题,常用的解决方法就是拆分解耦,把业务操作 (Extractor) 跟具体的数据结构解耦 ,设计成独立的类。

如下:

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
37
38
39
40
41
42
43
44
45
46
47
48
public abstract class ResourceFile {
protected String filePath;
public ResourceFile(String filePath) {
this.filePath = filePath;
}
}

public class PdfFile extends ResourceFile {
public PdfFile(String filePath) {
super(filePath);
}
}

// PPTFile... WordFile...

public class Extractor {
public void extract2txt(PPTFile pptFile) {
System.out.println("Extract PPT.");
}

public void extract2txt(PdfFile pdfFile) {
System.out.println("Extract PDF.");
}

public void extract2txt(WordFile wordFile) {
System.out.println("Extract WORD.");
}
}

public class ToolApplication {
public static void main(String[] args) {
Extractor extractor = new Extractor();
List<ResourceFile> resourceFiles = listAllResourceFiles();
for (ResourceFile resourceFile : resourceFiles) {
// 报错
// extractor.extract2txt(resourceFile);
}
}

private static List<ResourceFile> listAllResourceFiles() {
List<ResourceFile> resourceFiles = new ArrayList<>();
//...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
resourceFiles.add(new PdfFile("a.pdf"));
resourceFiles.add(new WordFile("b.word"));
resourceFiles.add(new PPTFile("c.ppt"));
return resourceFiles;
}
}

这其中最关键的一点设计是,把抽取文本内容的操作,设计成了三个重载函数。

但是,在实际使用 extract2txt时, 编译报错了,为什么呢?

因为多态是一种动态绑定,可以在运行时获取对象的实际类型,来运行实际类型对应的方法。而函数重载是一种静态绑定,在编译时并不能获取对象的实际类型,而是根据声明类型执行声明类型对应的方法。上面 resourceFiles 所对应的对象的类型是 ResourceFile,而我们并没有在 Extractor 类中定义参数类型是 ResourceFileextract2txt() 重载函数,所以在编译阶段就通过不了,更别说在运行时根据对象的实际类型执行不同的重载函数了。

解决方法如下:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public abstract class ResourceFile {
protected String filePath;

public ResourceFile(String filePath) {
this.filePath = filePath;
}

abstract public void accept(Extractor extractor);
abstract public void accept(Compressor compressor);
}

public class PdfFile extends ResourceFile {
public PdfFile(String filePath) {
super(filePath);
}

@Override
public void accept(Extractor extractor) {
extractor.extract2txt(this);
}

@Override
public void accept(Compressor compressor) {
compressor.compress(this);
}
}

// PPTFile... WordFile...

public class Extractor {
public void extract2txt(PPTFile pptFile) {
System.out.println("Extract PPT.");
}

public void extract2txt(PdfFile pdfFile) {
System.out.println("Extract PDF.");
}

public void extract2txt(WordFile wordFile) {
System.out.println("Extract WORD.");
}
}

public class Compressor {
public void compress(PPTFile pptFile) {
System.out.println("Extract PPT.");
}

public void compress(PdfFile pdfFile) {
System.out.println("Extract PDF.");
}

public void compress(WordFile wordFile) {
System.out.println("Extract WORD.");
}
}

public class ToolApplication {
public static void main(String[] args) {
Extractor extractor = new Extractor();
List<ResourceFile> resourceFiles = listAllResourceFiles();
for (ResourceFile resourceFile : resourceFiles) {
resourceFile.accept(extractor);
}
Compressor compressor = new Compressor();
for (ResourceFile resourceFile : resourceFiles) {
resourceFile.accept(compressor);
}
}

private static List<ResourceFile> listAllResourceFiles() {
List<ResourceFile> resourceFiles = new ArrayList<>();
//...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
resourceFiles.add(new PdfFile("a.pdf"));
resourceFiles.add(new WordFile("b.word"));
resourceFiles.add(new PPTFile("c.ppt"));
return resourceFiles;
}
}

多态特性,程序会调用实际类型的 accept 函数,比如 PdfFileaccept 函数,然后将 this 传到对应的 extract2txt,compress中, 这里的 this 就是 PdfFile ,在编译的时候就确定了,所以会调用 extractorextract2txt(PdfFile pdfFile) 这个重载函数。

访问者模式优化

其实这样写还是有问题的,添加一个新的业务,还是需要修改每个资源文件类,违反了开闭原则。针对这个问题,我们抽象出来一个 Visitor 接口,包含是三个命名非常通用的 visit() 重载函数,分别处理三种不同类型的资源文件。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public interface Visitor {
void visit(PdfFile pdfFile);

void visit(PPTFile pdfFile);

void visit(WordFile pdfFile);
}

public abstract class ResourceFile {
protected String filePath;

public ResourceFile(String filePath) {
this.filePath = filePath;
}

abstract public void accept(Visitor visitor);
}

public class PdfFile extends ResourceFile {
public PdfFile(String filePath) {
super(filePath);
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}

// PPTFile... WordFile...

public class Extractor implements Visitor{

@Override
public void visit(PPTFile pptFile) {
System.out.println("Extract PPT.");
}

@Override
public void visit(PdfFile pdfFile) {
System.out.println("Extract PDF.");
}

@Override
public void visit(WordFile wordFile) {
System.out.println("Extract WORD.");
}
}

// Compressor...

public class ToolApplication {
public static void main(String[] args) {
Extractor extractor = new Extractor();
List<ResourceFile> resourceFiles = listAllResourceFiles();
for (ResourceFile resourceFile : resourceFiles) {
resourceFile.accept(extractor);
}
Compressor compressor = new Compressor();
for (ResourceFile resourceFile : resourceFiles) {
resourceFile.accept(compressor);
}
}

private static List<ResourceFile> listAllResourceFiles() {
List<ResourceFile> resourceFiles = new ArrayList<>();
//...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
resourceFiles.add(new PdfFile("a.pdf"));
resourceFiles.add(new WordFile("b.word"));
resourceFiles.add(new PPTFile("c.ppt"));
return resourceFiles;
}
}

Extractor 负责抽取文本内容,Compressor 负责压缩。当我们新添加一个业务功能的时候,资源文件类不需要做任何修改,只需要修改 ToolApplication 的代码就可以了。

感谢

设计模式之美

以及上文中的链接