程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 運用設計模式設計MIME編碼類 -- 兼談Template Method和Strategy

運用設計模式設計MIME編碼類 -- 兼談Template Method和Strategy

編輯:關於VC++

本文講述可重用、易擴充的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個模式的根本區別。

順便說明,《設計模式》一書中非常強調對象模式和類模式的區別,本文就提供了一個很極端的例子——用對象模式可行而用類模式不可行。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
欄目導航
Copyright © 程式師世界 All Rights Reserved