1、概述
我们先来看两种模式的通用类图。
两者之间确实很相似。如果把策略模式的环境角色变更为一个抽象类加一个实现类,或者桥梁模式的抽象角色未实现,只有修正抽象化角色,想想看,这两个类图有什么地方不一样?完全一样!正是由于类似场景的存在才导致了两者在实际应用中经常混淆的情况发生,我们来举例说明两者有何差别。
大家都知道邮件有两种格式:文本邮件(TextMail)和超文本邮件(HTMLMaiL),在文本邮件中只能有简单的文字信息,而在超文本邮件中可以有复杂文字(带有颜色、字体等属性)、图片、视频等,如果你使用Foxmail邮件客户端的话就应该有深刻体验,看到一份邮件,怎么没内容?原来是你忘记点击那个“HTML邮件”标签了。下面我们就来讲解如何发送这两种不同格式的邮件,研究一下这两种模式如何处理这样的场景。
2、策略模式实现邮件发送
2.1 类图
使用策略模式发送邮件,我们认为这两种邮件是两种不同的封装格式,给定了发件人、收件人、标题、内容的一封邮件,按照两种不同的格式分别进行封装,然后发送之。按照这样的分析,我们发现邮件的两种不同封装格式就是两种不同的算法,具体到策略模式就是两种不同策略,这样看已经很简单了,我们可以直接套用策略模式来实现。
我们定义了一个邮件模板,它有两个实现类:TextMail(文本邮件)和HtmlMail(超文本邮件),分别实现两种不同格式的邮件封装。MailServer是一个环境角色,它接收一个MailTemplate对象,然后通过sendMail方法发送出去。
2.2 代码
2.2.1 抽象邮件
class CMailTemplate { public: CMailTemplate(const string &sFrom, const string &sTo, const string &sSubject , const string &sContent) : msFrom(sFrom), msTo(sTo), msSubject(sSubject), msContent(sContent){} ~CMailTemplate(){}; void mvSetFrom(const string &sFrom) { msFrom = sFrom; } string msGetFrom() { return msFrom; } void mvSetTo(const string &sTo) { msTo = sTo; } string msGetTo() { return msTo; } void mvSetSubject(const string &sSubject) { msSubject = sSubject; } string msGetSubject() { return msSubject; } void mvSetContent(const string &sContent) { msContent = sContent; } virtual string msGetContent(){ return msContent; } private: string msFrom; //邮件发件人 string msTo; //收件人 string msSubject; //邮件标题 string msContent; //通过构造函数传递邮件信息 };
抽象类没有抽象的方法,设置为抽象类还有什么意义呢?有意义,在这里我们定义了一个这样的抽象类:它具有邮件的所有属性,但不是一个具体可以被实例化的对象。例如,你对邮件服务器说“给我制造一封邮件”,邮件服务器肯定拒绝,为什么?你要产生什么邮件?什么格式的?邮件对邮件服务器来说是一个抽象表示,是一个可描述但不可形象化的事物。你可以这样说:“我要一封标题为XX,发件人是XXX的文本格式的邮件”,这就是一个可实例化的对象,因此我们的设计就产生了两个子类以具体化邮件,而且每种邮件格式对邮件的内容都有不同的处理。
2.2.2 文本邮件
class CTextMail : public CMailTemplate { public: CTextMail(const string &sFrom, const string &sTo, const string &sSubject, const string &sContent) : CMailTemplate(sFrom, sTo, sSubject, sContent) {} ~CTextMail() {} string msGetContent() { //文本类型设置邮件的格式为: text/plain string s_content = " Content-Type: text/plain;charset=GB2312 " + CMailTemplate::msGetContent(); //同时对邮件进行base64编码处理,这里用一句话代替 s_content += " 邮件格式为: 文本格式"; return s_content; } };
我们覆写了msGetContent方法,因为要把一封邮件设置为文本邮件必须加上一个特殊的标志:text/plain,用于告诉解析这份邮件的客户端:“我是一封文本格式的邮件,别解析错了”。
2.2.3 超文本邮件
同样,超文本格式的邮件也有类似的设置。
class CHtmlMail : public CMailTemplate { public: CHtmlMail(const string &sFrom, const string &sTo, const string &sSubject, const string &sContent) : CMailTemplate(sFrom, sTo, sSubject, sContent) {} ~CHtmlMail() {} string msGetContent() { //超文本类型设置邮件的格式为: multipart/mixed string s_content = " Content-Type: multipart/mixed; charset= GB2312 " + CMailTemplate::msGetContent(); //同时对邮件进行base64编码处理,这里用一句话代替 s_content += " 邮件格式为: 超文本格式"; return s_content; } };
优秀一点的邮件客户端会对邮件的格式进行检查,比如编写一封超文本格式的邮件,在内容中加上了<font>标签,但是遗忘了</font>结尾标签,邮件的产生者(也就是邮件的客户端)会提示进行修正,我们这里用了“邮件格式为:超文本格式”来代表该逻辑。两个实现类实现了不同的算法,给定相同的发件人、收件人、标题和内容可以产生不同的邮件信息。
2.2.4 邮件服务器
class CMailServer { public: CMailServer(CMailTemplate *opMail) : mopMail(opMail) {} ~CMailServer(){} //发送邮件 void mvSendMail() { cout << "====正在发送的邮件信息====" << endl; //发件人 cout << "发件人: " << mopMail->msGetFrom().c_str() << endl; //收件人 cout << "收件人:" << mopMail->msGetTo().c_str() << endl; //标题 cout << "邮件标题: " << mopMail->msGetSubject().c_str() << endl; //邮件内容 cout << "邮件内容: " << mopMail->msGetContent().c_str() << endl; } private: //发送的是哪封邮件 CMailTemplate *mopMail; };
很简单,邮件服务器接收了一封邮件,然后调用自己的发送程序进行发送。有人可能要问了,为什么不把mvSendMail方法移植到邮件模板类中呢?这也是邮件模板类的一个行为,邮件可以被发送。是的,这确实是邮件的一个行为,完全可以这样做,两者没有什么区别,只是从不同的角度看待该方法而已。
2.2.5 场景调用
int main() { //创建一封TEXT格式的邮件 CMailTemplate *op_mail = new CHtmlMail("a@a.com", "b@b.com", "外星人攻击地球了", "结果是外星人被地球人打败了!"); //创建一个Mail发送程序 CMailServer *op_server = new CMailServer(op_mail); op_server->mvSendMail(); return 0; }
2.2.6 执行结果
当然,如果想产生一封文本格式的邮件,只要稍稍修改一下场景类就可以了:new CHtmlMail修改为new CTextMail,非常简单。在该场景中,我们使用策略模式实现两种算法的自由切换,它提供了这样的保证:封装邮件的两种行为是可选择的,至于选择哪个算法是由上层模块决定的。策略模式要完成的任务就是提供两种可以替换的算法。
3、桥梁模式实现邮件发送
3.1 类图
桥梁模式关注的是抽象和实现的分离,它是结构型模式,结构型模式研究的是如何建立一个软件架构,下面我们就来看看桥梁模式是如何构件一套发送邮件的架构的。
类图中我们增加了SendMail和Postfix两个邮件服务器来实现类,在邮件模板中允许增加发送者标记,其他与策略模式都相同。我们在这里已经完成了一个独立的架构,邮件有了,发送邮件的服务器也具备了,是一个完整的邮件发送程序。需要注意的是,SendMail类不是一个动词行为(发送邮件),它指的是一款开源邮件服务器产品,一般*nix系统的默认邮件服务器就是SendMail;Postfix也是一款开源的邮件服务器产品,其性能、稳定性都在逐步赶超SendMail。
3.2 代码
3.2.1 邮件模板
我们来看代码实现,邮件模板仅仅增加了一个add方法,文本邮件、超文本邮件都没有任何改变。
//邮件模板 class CMailTemplate { public: CMailTemplate(const string &sFrom, const string &sTo, const string &sSubject, const string &sContent) : msFrom(sFrom), msTo(sTo), msSubject(sSubject), msContent(sContent){} ~CMailTemplate(){}; void mvSetFrom(const string &sFrom) { msFrom = sFrom; } string msGetFrom() { return msFrom; } void mvSetTo(const string &sTo) { msTo = sTo; } string msGetTo() { return msTo; } void mvSetSubject(const string &sSubject) { msSubject = sSubject; } string msGetSubject() { return msSubject; } void mvSetContent(const string &sContent) { msContent = sContent; } virtual string msGetContent(){ return msContent; } //允许增加邮件发送标志 void mvAdd(const string &sSendInfo) { msContent = sSendInfo + msContent; } private: string msFrom; //邮件发件人 string msTo; //收件人 string msSubject; //邮件标题 string msContent; //通过构造函数传递邮件信息 }; //文本邮件 class CTextMail : public CMailTemplate { public: CTextMail(const string &sFrom, const string &sTo, const string &sSubject, const string &sContent) : CMailTemplate(sFrom, sTo, sSubject, sContent) {} ~CTextMail() {} string msGetContent() { //文本类型设置邮件的格式为: text/plain string s_content = " Content-Type: text/plain;charset=GB2312 " + CMailTemplate::msGetContent(); //同时对邮件进行base64编码处理,这里用一句话代替 s_content += " 邮件格式为: 文本格式"; return s_content; } }; //超文本邮件 class CHtmlMail : public CMailTemplate { public: CHtmlMail(const string &sFrom, const string &sTo, const string &sSubject, const string &sContent) : CMailTemplate(sFrom, sTo, sSubject, sContent) {} ~CHtmlMail() {} string msGetContent() { //超文本类型设置邮件的格式为: multipart/mixed string s_content = " Content-Type: multipart/mixed; charset= GB2312 " + CMailTemplate::msGetContent(); //同时对邮件进行base64编码处理,这里用一句话代替 s_content += " 邮件格式为: 超文本格式"; return s_content; } };
3.2.2 邮件服务器
我们来看邮件服务器,也就是桥梁模式的抽象化角色。
class CMailServer { public: CMailServer(CMailTemplate *opMail) : mopMail(opMail) {}; ~CMailServer(){}; //发送邮件 virtual void mvSendMail() { cout << "====正在发送的邮件信息====" << endl; //发件人 cout << "发件人:" << mopMail->msGetFrom().c_str() << endl; //收件人 cout << "收件人:" << mopMail->msGetTo().c_str() << endl; //标题 cout << "邮件标题: " << mopMail->msGetSubject().c_str() << endl; //邮件内容 cout << "邮件内容: " << mopMail->msGetContent().c_str() << endl; } protected: //发送的是哪封邮件 CMailTemplate *mopMail; };
该类相对于策略模式的环境角色有两个改变:
● 修改为抽象类。为什么要修改成抽象类?因为我们在设计一个架构,邮件服务器是一个具体的、可实例化的对象吗?“给我一台邮件服务器”能实现吗?不能,只能说“给我一台Postfix邮件服务器”,这才能实现,必须有一个明确的可指向对象。
● 变量m修改为Protected访问权限,方便子类调用。
3.2.3 Postfix邮件服务器
class CPostfix : public CMailServer { public: CPostfix(CMailTemplate *opMail) : CMailServer(opMail) {} ~CPostfix(){} //修正邮件发送程序 void mvSendMail() { //增加邮件服务器信息 string s_content = "Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com(Postfix) with ESMTP id 8DBCB1456B8 "; mopMail->mvAdd(s_content); CMailServer::mvSendMail(); } };
3.2.4 SendMail邮件服务器
为什么要覆写mvSendMail程序呢?这是因为每个邮件服务器在发送邮件时都会在邮件内容上留下自己的标志,一是广告作用,二是为了互联网上统计需要,三是方便同质软件的共振。
class CSendMail : public CMailServer { public: CSendMail(CMailTemplate *opMail) : CMailServer(opMail) {} ~CSendMail(){} //修正邮件发送程序 void mvSendMail() { //增加邮件服务器信息 string s_content = "Received: (sendmail); 7 Nov 2009 04:14:44 +0100"; mopMail->mvAdd(s_content); CMailServer::mvSendMail(); } };
3.2.5 场景调用
邮件和邮件服务器都有了,我们来看怎么发送邮件。
int main() { //创建一封TEXT格式的邮件 CMailTemplate *op_mail = new CTextMail("a@a.com", "b@b.com", "外星人攻击地球了", " 结果地球人打败了外星人!"); //使用Postfix发送邮件 CMailServer *op_post = new CPostfix(op_mail); //发送邮件 op_post->mvSendMail(); return 0; }
3.2.6 运行结果
当然了,还有其他三种发送邮件的方式:Postfix发送文本邮件以及SendMail发送文本邮件和超文本邮件。
4、总结
策略模式和桥梁模式是如此相似,我们只能从它们的意图上来分析。策略模式是一个行为模式,旨在封装一系列的行为,在例子中我们认为把邮件的必要信息(发件人、收件人、标题、内容)封装成一个对象就是一个行为,封装的格式(算法)不同,行为也就不同。而桥梁模式则是解决在不破坏封装的情况下如何抽取出它的抽象部分和实现部分,它的前提是不破坏封装,让抽象部分和实现部分都可以独立地变化,在例子中,我们的邮件服务器和邮件模板是不是都可以独立地变化?不管是邮件服务器还是邮件模板,只要继承了抽象类就可以继续扩展,它的主旨是建立一个不破坏封装性的可扩展架构。
简单来说,策略模式是使用继承和多态建立一套可以自由切换算法的模式,桥梁模式是在不破坏封装的前提下解决抽象和实现都可以独立扩展的模式。桥梁模式必然有两个“桥墩”——抽象化角色和实现化角色,只要桥墩搭建好,桥就有了,而策略模式只有一个抽象角色,可以没有实现,也可以有很多实现。
还是很难区分,是吧?多想想两者的意图,就可以理解为什么要建立两个相似的模式了。我们在做系统设计时,可以不考虑到底使用的是策略模式还是桥梁模式,只要好用,能够解决问题就成,“不管黑猫白猫,抓住老鼠的就是好猫”。