33.1 策略模式VS桥梁模式

33.1 策略模式VS桥梁模式

这对冤家终于碰头了,策略模式与桥梁模式是如此相似,简直就是孪生兄弟,要把它们两个分开可不太容易。我们来看看两者的通用类图,如图33-1所示。

image-20210930184916250

图33-1 策略模式(左)和桥梁模式(右)通用类图
两者之间确实很相似。如果把策略模式的环境角色变更为一个抽象类加一个实现类,或者桥梁模式的抽象角色未实现,只有修正抽象化角色,想想看,这两个类图有什么地方不一样?完全一样!正是由于类似场景的存在才导致了两者在实际应用中经常混淆的情况发生, 我们来举例说明两者有何差别。

大家都知道邮件有两种格式:文本邮件(Text Mail)和超文本邮件(HTML MaiL),在文本邮件中只能有简单的文字信息,而在超文本邮件中可以有复杂文字(带有颜色、字体等属性)、图片、视频等,如果你使用Foxmail邮件客户端的话就应该有深刻体验,看到一份邮件,怎么没内容?原来是你忘记点击那个“HTML邮件”标签了。下面我们就来讲解如何发送这两种不同格式的邮件,研究一下这两种模式如何处理这样的场景。

33.1.1 策略模式实现邮件发送

使用策略模式发送邮件,我们认为这两种邮件是两种不同的封装格式,给定了发件人、 收件人、标题、内容的一封邮件,按照两种不同的格式分别进行封装,然后发送之。按照这样的分析,我们发现邮件的两种不同封装格式就是两种不同的算法,具体到策略模式就是两种不同策略,这样看已经很简单了,我们可以直接套用策略模式来实现。先看类图,如图33-2所示。

image-20210930200945323

图33-2 策略模式实现邮件发送的类图

我们定义了一个邮件模板,它有两个实现类:TextMail(文本邮件)和HtmlMail(超文本邮件),分别实现两种不同格式的邮件封装。MailServer是一个环境角色,它接收一个MailTemplate对象,然后通过sendMail方法发送出去。我们来看具体的代码,先看抽象邮件,如代码清单33-1所示。

代码清单33-1 抽象邮件

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
public abstract class MailTemplate {
//邮件发件人
private String from;
//收件人
private String to;
//邮件标题
private String subject;
//邮件内容
private String context;
//通过构造函数传递邮件信息
public MailTemplate(String _from,String _to,String _subject,String _context){
this.from = _from;
this.to = _to;
this.subject = _subject;
this.context = _context;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public void setContext(String context){
this.context = context;
}
//邮件都有内容
public String getContext(){
return context;
}
}

很奇怪,是吗?抽象类没有抽象的方法,设置为抽象类还有什么意义呢?有意义,在这里我们定义了一个这样的抽象类:它具有邮件的所有属性,但不是一个具体可以被实例化的对象。例如,你对邮件服务器说“给我制造一封邮件”,邮件服务器肯定拒绝,为什么?你要产生什么邮件?什么格式的?邮件对邮件服务器来说是一个抽象表示,是一个可描述但不可形象化的事物。你可以这样说:“我要一封标题为XX,发件人是XXX的文本格式的邮件”, 这就是一个可实例化的对象,因此我们的设计就产生了两个子类以具体化邮件,而且每种邮件格式对邮件的内容都有不同的处理。我们首先看文本邮件,如代码清单33-2所示。

代码清单33-2 文本邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TextMail extends MailTemplate {
public TextMail(String _from, String _to, String _subject, String _context) {
super(_from, _to, _subject, _context);
}
public String getContext() {
//文本类型设置邮件的格式为:text/plain
String context = "\nContent-Type: text/plain;
charset=GB2312\n" +super.getContext();
//同时对邮件进行base64编码处理,这里用一句话代替
context = context + "\n邮件格式为:文本格式";
return context;
}
}

我们覆写了getContext方法,因为要把一封邮件设置为文本邮件必须加上一个特殊的标志:text/plain,用于告诉解析这份邮件的客户端:“我是一封文本格式的邮件,别解析错了”。同样,超文本格式的邮件也有类似的设置,如代码清单33-3所示。

代码清单33-3 超文本邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HtmlMail extends MailTemplate {
public HtmlMail(String _from, String _to, String _subject, String _context) {
super(_from, _to, _subject, _context);
}
public String getContext(){
//超文本类型设置邮件的格式为:multipart/mixed
String context = "\nContent-Type: multipart/mixed;
charset= GB2312\n" +super.getContext();
//同时对邮件进行HTML检查,是否有类似未关闭的标签
context = context + "\n邮件格式为:超文本格式";
return context;
}
}

优秀一点的邮件客户端会对邮件的格式进行检查,比如编写一封超文本格式的邮件,在内容中加上了标签,但是遗忘了结尾标签,邮件的产生者(也就是邮件的客户端)会提示进行修正,我们这里用了“邮件格式为:超文本格式”来代表该逻辑。

两个实现类实现了不同的算法,给定相同的发件人、收件人、标题和内容可以产生不同的邮件信息。我们看看邮件是如何发送出去的,如代码清单33-4所示。

代码清单33-4 邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MailServer {
//发送的是哪封邮件
private MailTemplate m;
public MailServer(MailTemplate _m){
this.m = _m;
}
//发送邮件
public void sendMail(){
System.out.println("====正在发送的邮件信息====");
//发件人
System.out.println("发件人:" + m.getFrom());
//收件人
System.out.println("收件人:" + m.getTo());
//标题
System.out.println("邮件标题:" + m.getSubject());
//邮件内容
System.out.println("邮件内容:" + m.getContext());
}
}

很简单,邮件服务器接收了一封邮件,然后调用自己的发送程序进行发送。可能读者要问了,为什么不把sendMail方法移植到邮件模板类中呢?这也是邮件模板类的一个行为,邮件可以被发送。是的,这确实是邮件的一个行为,完全可以这样做,两者没有什么区别,只是从不同的角度看待该方法而已。我们继续看场景类,如代码清单33-5所示。

代码清单33-5 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//创建一封TEXT格式的邮件
MailTemplate m = new HtmlMail("a@a.com","b@b.com","外星人攻击地球了","结局是外星人被地球人打败了!");
//创建一个Mail发送程序
MailServer mail = new MailServer(m);
//发送邮件
mail.sendMail();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
====正在发送的邮件信息==== 
发件人:a@a.com
收件人:b@b.com
邮件标题:外星人攻击地球了
邮件内容:
Content-Type: multipart/mixed;charset=GB2312
结局是外星人被地球人打败了!
邮件格式为:超文本格式

当然,如果想产生一封文本格式的邮件,只要稍稍修改一下场景类就可以了:new HtmlMail修改为new TextMail,读者可以自行实现,非常简单。在该场景中,我们使用策略模式实现两种算法的自由切换,它提供了这样的保证:封装邮件的两种行为是可选择的,至于选择哪个算法是由上层模块决定的。策略模式要完成的任务就是提供两种可以替换的算法。

33.1.2 桥梁模式实现邮件发送

桥梁模式关注的是抽象和实现的分离,它是结构型模式,结构型模式研究的是如何建立一个软件架构,下面我们就来看看桥梁模式是如何构件一套发送邮件的架构的,如图33-3所示。

类图中我们增加了SendMail和Postfix两个邮件服务器来实现类,在邮件模板中允许增加发送者标记,其他与策略模式都相同。我们在这里已经完成了一个独立的架构,邮件有了, 发送邮件的服务器也具备了,是一个完整的邮件发送程序。需要读者注意的是,SendMail类不是一个动词行为(发送邮件),它指的是一款开源邮件服务器产品,一般*nix系统的默认邮件服务器就是SendMail;Postfix也是一款开源的邮件服务器产品,其性能、稳定性都在逐步赶超SendMail。

image-20210930202528940

图33-3 桥梁模式实现邮件发送的类图

我们来看代码实现,邮件模板仅仅增加了一个add方法,如代码清单33-6所示。

代码清单33-6 邮件模板

1
2
3
4
5
6
7
8
9
public abstract class MailTemplate {
/*
*该部分代码不变,请参考代码清单33-1
*/
//允许增加邮件发送标志
public void add(String sendInfo){
context = sendInfo + context;
}
}

文本邮件、超文本邮件都没有任何改变,如代码清单33-2、33-3所示,这里不再赘述。

我们来看邮件服务器,也就是桥梁模式的抽象化角色,如代码清单33-7所示。

代码清单33-7 邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class MailServer {
//发送的是哪封邮件
protected final MailTemplate m;
public MailServer(MailTemplate _m){
this.m = _m;
}
//发送邮件
public void sendMail(){
System.out.println("====正在发送的邮件信息====");
//发件人
System.out.println("发件人:" + m.getFrom());
//收件人
System.out.println("收件人:" + m.getTo());
//标题
System.out.println("邮件标题:" + m.getSubject());
//邮件内容
System.out.println("邮件内容:" + m.getContext());
}
}

该类相对于策略模式的环境角色有两个改变:

  • 修改为抽象类。为什么要修改成抽象类?因为我们在设计一个架构,邮件服务器是一 个具体的、可实例化的对象吗?“给我一台邮件服务器”能实现吗?不能,只能说“给我一台 Postfix邮件服务器”,这才能实现,必须有一个明确的可指向对象。
  • 变量m修改为Protected访问权限,方便子类调用。

我们再来看看Postfix邮件服务器的实现,如代码清单33-8所示。

代码清单33-8 Postfix邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
public class Postfix extends MailServer {
public Postfix(MailTemplate _m) {
super(_m);
}
//修正邮件发送程序
public void sendMail(){
//增加邮件服务器信息
String context ="Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com (Postfix) with ESMTP id 8DBCD172B8\n" ;
super.m.add(context);
super.sendMail();
}
}

为什么要覆写sendMail程序呢?这是因为每个邮件服务器在发送邮件时都会在邮件内容上留下自己的标志,一是广告作用,二是为了互联网上统计需要,三是方便同质软件的共振。我们再来看SendMail邮件服务器的实现,如代码清单33-9所示。

代码清单33-9 SendMail邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SendMail extends MailServer {
//传递一封邮件
public SendMail(MailTemplate _m) {
super(_m);
}
//修正邮件发送程序
@Override
public void sendMail(){
//增加邮件服务器信息
super.m.add("Received: (sendmail);7 Nov 2009 04:14:44 +0100");
super.sendMail();
}
}

邮件和邮件服务器都有了,我们来看怎么发送邮件,如代码清单33-10所示。

代码清单33-10 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//创建一封TEXT格式的邮件
MailTemplate m = new HtmlMail("a@a.com","b@b.com","外星人攻击地球了"," 结局是外星人被地球人打败了!");
//使用Postfix发送邮件
MailServer mail = new Postfix(m);
//发送邮件
mail.sendMail();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
====正在发送的邮件信息==== 
发件人:a@a.com
收件人:b@b.com
邮件标题:外星人攻击地球了
邮件内容: Content-Type: multipart/mixed;charset=GB2312
Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com (Postfix) with ESMTP id 8DBCD172B8
结局是外星人被地球人打败了!
邮件格式为:超文本格式

当然了,还有其他三种发送邮件的方式:Postfix发送文本邮件以及SendMail发送文本邮件和超文本邮件。修改量很小,读者可以自行修改实现,体会一下桥梁模式的特点。

33.1.3 最佳实践

策略模式和桥梁模式是如此相似,我们只能从它们的意图上来分析。策略模式是一个行为模式,旨在封装一系列的行为,在例子中我们认为把邮件的必要信息(发件人、收件人、 标题、内容)封装成一个对象就是一个行为,封装的格式(算法)不同,行为也就不同。而桥梁模式则是解决在不破坏封装的情况下如何抽取出它的抽象部分和实现部分,它的前提是不破坏封装,让抽象部分和实现部分都可以独立地变化,在例子中,我们的邮件服务器和邮件模板是不是都可以独立地变化?不管是邮件服务器还是邮件模板,只要继承了抽象类就可以继续扩展,它的主旨是建立一个不破坏封装性的可扩展架构。

简单来说,策略模式是使用继承和多态建立一套可以自由切换算法的模式,桥梁模式是在不破坏封装的前提下解决抽象和实现都可以独立扩展的模式。桥梁模式必然有两个“桥墩”——抽象化角色和实现化角色,只要桥墩搭建好,桥就有了,而策略模式只有一个抽象角色,可以没有实现,也可以有很多实现。

还是很难区分,是吧?多想想两者的意图,就可以理解为什么要建立两个相似的模式了。我们在做系统设计时,可以不考虑到底使用的是策略模式还是桥梁模式,只要好用,能够解决问题就成,“不管黑猫白猫,抓住老鼠的就是好猫”。