運用設計模式設計MIME編碼類別 — 兼談Template Method和Strategy模式的區別

來源:互聯網
上載者:User

作者: 溫昱 (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

作者資訊:
姓名:溫昱
郵箱:xinxiu123@sina.com
網站:lcspace.nease.net

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.