上一个小节给出了一个方案,对于只有GUISys这一个错误处理者是很完美的,但是情况往往不是这样的。前面也曾经提到过,对于发生的错误,除了要通知系统的使用者外,还要进行其他的处理,比如:试图恢复,记如日志等。可以看出,这些处理方法和把错误通告给使用者是非常不同的,完全没有办法仅仅用一个handle方法来统一所有的不同的处理。但是,如果我们在ErrorBase中增加不同的处理方法声明,在具体的错误类中,根据自身的需要来相应的实现这些方法,好像也是一个不错的方案。代码示例如下:
2
3 {
4
5 public void guiHandle();
6
7 public void logHandle();
8
9 }
10
11 class DBAccessError implements ErrorBase
12
13 {
14
15 public void guiHandle() {
16
17 /* 通知用户界面的数据库访问错误处理 */
18
19 }
20
21 public void logHandle() {
22
23 /* 通知日志系统的数据库访问错误处理 */
24
25 }
26
27 }
28
29 class CommunicationError implements ErrorBase
30
31 {
32
33 public void guiHandle() {
34
35 /* 通知用户界面的通信错误处理 */
36
37 }
38
39 public void logHandle() {
40
41 /* 通知日志系统的通信错误处理 */
42
43 }
44
45 }
46
47 class GUISys
48
49 {
50
51 public void announceError(ErrorBase error) {
52
53 error.guiHandle();
54
55 }
56
57 }
58
59 class LogSys
60
61 {
62
63 public void announceError(ErrorBase error) {
64
65 error.logHandle();
66
67 }
68
69 }
70
71
读者可能已经注意到,这种做法其实也不是十分符合OCP,虽然它把变化局限在ErrorBase这个类层次架构中,但是增加新的处理方法,还是更改了已经存在的ErrorBase类。其实,这种设计方法,还违反了另外一个著名的面向对象的设计原则:SRP(Single Responsibility Principle)。这个原则的核心含意是:一个类应该有且仅有一个职责。关于职责的含意,面向对象大师Robert.C Martin有一个著名的定义:所谓一个类的职责是指引起该类变化的原因,如果一个类具有一个以上的职责,那么就会有多个不同的原因引起该类变化,其实就是耦合了多个互不相关的职责,就会降低这个类的内聚性。错误类的职责就是,保存和自己相关的错误状态,并且提供方法用于获取这些状态。上面的设计中把不同的处理方法也放到错误类中,从而增加了错误类的职责,这样即使和错误类本身没有关系的对于错误处理方式的变化,增加、修改都会导致错误类的修改。这种设计方法一样会在需求变化时,带来没有预料到的问题。那么能否将对错误的处理方法从中剥离出来呢?如果读者比较熟悉设计模式(这里的熟悉是指,设计模式的意图、动机,而不是指怎样去实现一个具体的设计模式),应该会隐隐约约感觉到一个更好的设计方案即将出现。
设计模式浮出水面
让我们对问题重新描述一下:我们已经有了一个关于错误的类层次结构,现在我们需要在不改变这个类层次结构的前提下允许我们增加对于这个类层次的新的处理方法。听起来很耳熟吧,不错,这正是过于visitor设计模式的意图的描述。通过对于该模式动机的分析,我们很容易知道,要想使用visitor模式,需要定义两个类层次:一个对应于接收操作的元素的类层次(就是我们的错误类),另一个对应于定义对元素的操作的访问者(就是我们的对于错误的不同处理方法)。这样,我们就转换了问题视角,即把需要不同的错误处理方法的问题转变为需要不同的错误处理类,这样的结果就是我们可以通过增加新的模块(类)来增加新的错误处理方法,而不是通过增加新的错误处理方法(这样做,就势必要修改已经存在的类)。
一旦到了这一部,下面的工作就比较简单了,因为visitor模式已经为我们搭建了一个设计的上下文,此时我们就可以关注visitor模式的实现部分来指导我们下面的具体实现了。下面仅仅给出最终的程序结构的UML图以及代码示例,其中忽略了错误类中的属于错误本身的方法,各个具体的错误处理方法通过这些方法和具体的错误类对象交互,来完成各自的处理功能。
最终的设计的程序结构图

最终的代码示例
2
3 {
4
5 public void handle(ErrorHandler handler);
6
7 }
8
9 class DBError implements ErrorBase
10
11 {
12
13 public void handle(ErrorHandler handler) {
14
15 handler.handle(this);
16
17 }
18
19 }
20
21 class CommError implements ErrorBase
22
23 {
24
25 public void handle(ErrorHandler handler) {
26
27 handler.handle(this);
28
29 }
30
31 }
32
33 interface ErrorHandler
34
35 {
36
37 public void handle(DBrror dbError);
38
39 public void handle(CommError commError);
40
41 }
42
43 class GUISys implements ErrorHandler
44
45 {
46
47 public void announceError(ErrorBase error) {
48
49 error.handle(this);
50
51 }
52
53 public void handle(DBError dbError) {
54
55 /* 通知用户界面进行有关数据库错误的处理 */
56
57 }
58
59 public void handle(CommError commError) {
60
61 /* 通知用户界面进行有关通信错误的处理 */
62
63 }
64
65 }
66
67 class LogSys implements ErrorHandler
68
69 {
70
71 public void announceError(ErrorBase error) {
72
73 error.handle(this);
74
75 }
76
77 public void handle(DBError dbError) {
78
79 /* 通知日志系统进行有关数据库错误的处理 */
80
81 }
82
83 public void handle(CommError commError) {
84
85 /* 通知日志系统进行有关通信错误的处理 */
86
87 }
88
89 }
90
91
设计模式并不仅仅是一个有关特定问题的解决方案这个结果,它的意图以及它的动机往往更重要,因为一旦我们理解了一个设计模式的意图、动机,那么在设计过程中,就很容易的发现适用于我们自己的设计模式,从而大大简化设计工作,并且可以得到一个比较理想的设计方案。
另外,在学习设计模式的过程中,应该更加注意设计模式背后的东西,即具体设计模式所共有的的一些优秀的指导原则,这些原则在 参考文献[1]的第一章中有详细的论述,基本上有两点:
发现变化,封装变化
优先使用组合(Composition),而不是继承
如果注意从这些方面来学习、理解设计模式,就会得到一些比单个具体设计模式本身更有用的知识,并且即使在没有现成模式可用的情况下,我们也一样可以设计出一个好的系统来。