运用设计模式设计MIME编码类 -- 兼谈Template Method和Strategy模式的区别

作者: 温昱 (lcspace.nease.net)

 

下载本文示例源代码

 

 

本文讲述可重用、易扩充的MIME编码类的设计思路;并顺便对比了Template Method和Strategy模式的区别。

一、背景知识

MIME 是一种Internet协议,全称为“Multipurpose Internet Mail Extensions” ,中文名称为“多用途互联网邮件扩展”。其实,它的应用并不局限于收发Internet邮件——它已经成为Internet上传输多媒体信息的基本协议之 一。本文仅关心MIME的编码算法。

MIME编码的原理就是把 8 bit 的内容转换成 7 bit 的形式以能正确传输,在接收方收到之后,再将其还原成 8 bit 的内容。对邮件进行编码最初的原因是因为 Internet 上的很多网关不能正确传输8 bit 内码的字符,比如汉字等。MIME编码共有Base64、Quoted-printable、7bit、8bit和Binary等几种。

Base64算法将输入的字符串或一段数据编码成只含有{''A''-''Z'', ''a''-''z'', ''0''-''9'', ''+'', ''/''}这64个字符的串,''=''用于填充。其编码的方法是,将输入数据流每次取6 bit,用此6 bit的值(0-63)作为索引去查表,输出相应字符。这样,每3个字节将编码为4个字符(3×8 → 4×6);不满4个字符的以''=''填充。

Quoted-printable算法根据输入的字符串或字节范围进行编码,若是不需编码的字符,直接输出;若需要编码,则先输出''='',后面跟着以2个字符表示的十六进制字节值。

二、设计目标

我们计划开发一套MIME编码和解码的类,适用于可以想到的多种应用场合:
· Email客户端程序
· 乱码察看程序
· 图片等二进制对象存入XML文件

设计目标如下:
· 可重用
· 易使用
· 易扩充

三、设计过程

本部分分为下面3节,注意它们不是并列的3种设计方案,而是达到趋于合理的设计的思考过程:
· 设计成仅提供方法的Utility
· 设计成使用Template Method模式的String Class
· 设计成使用Strategy模式的String Class

1、设计成仅提供方法的Utility

首先跳进我脑子的想法就是设计成Utility(仅仅提供方法的类),我想可能是我受C影响太大的缘故吧。

它的接口会是什么样子呢?差不多象

bool UMime::Encode(unsigned char * outTargetBuf,
int & outTargetBufLen,
const unsigned char * const inSourceBuf,
int inSourceBufLen);
bool UMime::Decode(unsigned char * outTargetBuf,
int & outTargetBufLen,
const unsigned char * const inSourceBuf,
int inSourceBufLen);
吧。不行,为了满足易使用要求,应该支持CString类型的buffer吧,再增加2个接口函数
bool UMime::Encode(CString & outTargetStr,CString & inSourceStr);
bool UMime::Decode(CString & outTargetStr,CString & inSourceStr);
这样以来,UMime一共包括4个接口函数。

好像还不错?高兴得太早了。因为将来应用中很可能出现CString和unsigned char *协同工作的情形。比如应用从XML文件中读出一个字符串放到一个CString型变量中,而这个字符串是一个Bmp图片的MIME编码,它解码过后自然应放到unsigned char *的buffer中。所以我们还要增加下面4个接口函数:

bool UMime::Encode(CString & outTargetStr,
const unsigned char * const inSourceBuf,
int inSourceBufLen);

bool UMime::Decode(CString & outTargetStr,
const unsigned char * const inSourceBuf,
int inSourceBufLen);

bool UMime::Encode(unsigned char * outTargetBuf,
int & outTargetBufLen,
CString & inSourceStr);

bool UMime::Decode(unsigned char * outTargetBuf,
int & outTargetBufLen,
CString & inSourceStr);
以免用户类型转换之苦。

啊哈,这么8个极为相似的接口函数搅在一起,好像一团麻呀。可重用性似乎满足了,但易使用性和易扩展性完全谈不上。

2、设计成使用Template Method模式的String Class

第2种方案浮现在脑海中:
· 既然整个算法就是将一个Buffer转换成另一个Buffer,写成一个String Class是非常自然的设计
· 用Class的成员变量保存Target Buffer及其长度(因为Buffer中可能有’"0’),另外提供GetBuf()和GetBufLen()作为查询Target Buffer的接口
· 直接从构造函数传递Source Buffer的信息

该类大概象这样:

class CMimeString
{
public:
enum PROCESSTYPE
{
ENCODING = 0,
DECODING = 1
};
CMimeString(PROCESSTYPE inType, const unsigned char * const inBuf, int inBufLen);
CMimeString(PROCESSTYPE inType, CString & inStr);
virtual ~ CMimeString();
unsigned char * GetBuf( void );
int GetBufLen( void );
operator LPCTSTR() const;
};
哈,似乎很美妙。
· Source Buffer仍然支持unsigned char *和CString这 2种类型,而Target Buffer由CMimeString本身来管理不必用户操心了。
· 但具体应用不是对二进制对象进行编码时,可以不用foo( s.GetBuf() )而直接用foo( s ),因为operator LPCTSTR() const;自动负责类型转换。
· 直接从构造函数传递Source Buffer的信息,使得接口更为精简。
当具体使用CMimeString时,大概象这样:
CString   buf("sadfsdfsdf");
CMimeString mime(CMimeString::ENCODING, buf);
MessageBox( s );
看来易使用性不错,下面要着重解决易扩展性了。CMimeString的实现部分会象这样:
class CMimeString
{
protected:
unsigned char * mBuf;
int mBufLen;
virtual void Encode( unsigned char * inBuf, int inBufLen );
virtual void Decode( unsigned char * inBuf, int inBufLen );
};
其中的两个虚函数是专门为易扩展性准备的,要实现新的MIME编码算法,只需要从CMimeString继承一个子类:
class CBase64String : public CMimeString
{
protected:
virtual void Encode( unsigned char * inBuf, int inBufLen );
virtual void Decode( unsigned char * inBuf, int inBufLen );
};
类图如下:

这两个虚函数是在哪里被调用的呢?在基类的构造函数中。
CMimeString::CMimeString(WHICHTYPE inType, CString & inStr)
{
mBuf = 0;
mBufLen = 0;

if( inType == ENCODING )
{
Encode((unsigned char *)(inStr.operator LPCTSTR()), inStr.GetLength());
}
else if( inType == DECODING )
{
Decode((unsigned char *)(inStr.operator LPCTSTR()), inStr.GetLength());
}
}
看上去是很不错的Template Method模式的运用,但是有问题——因为“在构造函数中调用虚函数”并无多态特性!
CBase64String::CBase64String(PROCESSTYPE inType, CString inStr)
{
OnlyInitSelf();
}
之后
CString   buf("sadfsdfsdf");
CBase64String base64(CMimeString::ENCODING, buf);
MessageBox( base64 );
是不对的,仍然是基类的CMimeString::Encode()被调用了,而且OnlyInitSelf()在Encode()被调用之后才被调到。
是不是有些懊恼?别急。分析问题背后的问题:我们实际上是想用Template Method模式,而且是让构造函数扮演Template Method的角色,而它先天(C++本身决定的)就不是这块料。

现在,摆在面前的至少有2条道路。第1种方法是,坚持使用Template Method模式,但要增加一个接口函数扮演Template Method角色。这样一来,我们使用CMimeString时就不如“直接从构造函数传递参数”方便。第2种方法是,坚持直接从构造函数传递参数,放弃 Template Method模式,改用其它模式完成“改变算法”的职责。我决定采用第2种方法。

3、设计成使用Strategy模式的String Class

除了Template Method模式以为,Strategy模式也可以履行“改变算法”的职责,我们就用Strategy模式代替Template Method模式继续完成CMimeString的设计,类图如下:


新的CMimeString的类声明如下:

class CMimeString
{
public:
enum PROCESSTYPE
{
ENCODING = 0,
DECODING = 1
};
enum ENCODETYPE
{
WYMIME = 0,
BASE64 = 1
};
CMimeString(PROCESSTYPE inType, ENCODETYPE inAlgoType, CString & inStr);
CMimeString(PROCESSTYPE inType, ENCODETYPE inAlgoType,
unsigned char * inBuf, int inBufLen);
virtual ~CMimeString();
int GetBufLen(void);
unsigned char * GetBuf(void);
operator LPCTSTR() const;
};
CMimeAlgo的类声明如下:
class CMimeAlgo
{
public:
CMimeAlgo();
~CMimeAlgo();
virtual void Encode( unsigned char ** outBuf, int & outBufLen,
unsigned char * inSrcBuf, int inSrcLen );
virtual void Decode( unsigned char ** outBuf, int & outBufLen,
unsigned char * inSrcBuf, int inSrcLen );
};
CBase64Algo的类声明如下:
class CBase64Algo : public CMimeAlgo
{
public:
CBase64Algo();
~CBase64Algo();
virtual void Encode( unsigned char ** outBuf, int & outBufLen,
unsigned char * inSrcBuf, int inSrcLen );
virtual void Decode( unsigned char ** outBuf, int & outBufLen,
unsigned char * inSrcBuf, int inSrcLen );
};
具体使用Base64算法是会象这样:
CString buf("sdfsdsdfsdfsdf");
CMimeString base64( CMimeString::ENCODING, CMimeString::BASE64, buf );
MessageBox(base64);
哈哈,基本满意。

四、使用举例

下面编一个小程序,重在演示CMimeString的用法。有2点需要说明:
· 程序比较简单,仅支持Base64编码和解码;
· 而且对一个串进行解码时并没有检查它是否是合法的Base64编码的结果串(有些字符串是不可能成为Base64编码的结果的),因此对串someString解码后再编码得到的串anotherString可能和someString并不相同。

五、Template Method和Strategy模式的区别

上面的设计过程中,牵涉到Template Method和Strategy这2个设计模式,本部分对它们简要总结和对比。

1、Template Method模式Tips

·Tip 1:关键字:Skeleton。

·Tip 2:图:


·Tip 3:支持变化。Subclass可以只改变算法的特定步骤,而不改变和继续使用算法的Skeleton。图中黄色的Class就是后来写的,而且工作量很 小,只需Override相应的Virtual函数。其中的ConcreteClass3的改动量更小,它从已有的ConcreteClass1继承,只 Override其中的一个Virtual函数。

Template Method可以说是最常见的模式,在MFC中,全局函数AfxWndProc()就是一例。

·Tip 4:支持框架。著名的Framework方面的“好莱坞法则”(Don''t call us, we''ll call you )就是主要由Template Method支持的“反向控制”(Superclass调用Subclass的Method)产生的。

2、Strategy模式Tips

·Tip 1:关键字。Aalgorithm Family。

·Tip 2:图:


可以看到,为了达到“将Aalgorithm从Data分离出来”的目的,代价是Context和Strategy 2 个对象。

·Tip 3:实现和使用。

实例化问题。从图中可以看到,Context和ConcreteStrategy的实例化,都将由“Application工程师”负责。

case语句。“Application工程师”不写case语句了,改“Architecture工程师”要写了。有空研究一下Borland ObjectWindow的源码。

Borland ObjectWindow之Dialog验证用户输入合法性,用了Strategy模式:

·Tip 4:支持变化。Strategy lets the algorithm vary independently from clients that use it。图中的黄色Class就是假想后来扩充的。

·Tip 5:局限性。

Strategy and Context之间是紧耦合。Strategy and Context interact to implement the algorithm. A context may pass all data required by the algorithm to the strategy when the algorithm is called. Alternatively, the context can pass itself as an argument to Strategy operations. That lets the strategy call back on the context as required.

Strategy 对Clients不能完全透明。Clients must be aware of different Strategies。 Therefore you should use the Strategy pattern only when the variation in behavior is relevant to clients。想想看,Client要负责ConcreteStrategy(和Context)的实例化,正是决定选哪一个 ConcreteStrategy的过程,使得“Strategy对Clients不能完全透明”。

3、Template Method和Strategy模式的对比

对比如下:
· 相同点,都是行为型模式,目的都是方便地改变算法。
· 不同点,实现方式前者使用继承,称为类模式;后者使用委托,称为对象模式。

《设计模式》一书在讲到Template method模式和Strategy模式的关系时说:“模板方法使用继承来改变算法的一部分。Strategy使用委托来改变整个算法。”

“算法的一部分”和“整个算法”的区别,笔者认为“整个算法”是“算法的一部分”的特例(就象数学中全集是集合的特例),因此不是2个模式的根本区别。

“继承”和“委托”的区别,即“类模式”和“对象模式”的区别,笔者认为这是2个模式的根本区别。

顺便说明,《设计模式》一书中非常强调对象模式和类模式的区别,本文就提供了一个很极端的例子——用对象模式可行而用类模式不可行。

参考文献:
《MIME邮件面面观》 作者:bhw98 出处:www.csdn.net
《设计模式》 作者:Gamma等 译者:李英军等
《Pattern Tips》 作者:温昱 出处:lcspace.nease.net

作者信息:
姓名:温昱
邮箱:[email protected]
网站:lcspace.nease.net

 

你可能感兴趣的:(template)