NO IMAGE

    在前面的文章中我們比較過框架和設計模式,一般的應用程式框架中都會大量用到設計模式。應用程式開發框架允許從一個或一組類中繼承以便建立一個新的應用程式,重用現存類中幾乎所有的程式碼,並且覆蓋其中一個或多個函式以便自定義所需要的應用程式。

    應用程式開發框架中的一個基本的概念是模板方法( Template Method )模式,它是如此的常見以至於我們在使用時甚至不覺得這是個模式,達到了熟視無睹的程度。

    模板方法模式的一個重要特徵是它(模板方法,這裡不是模式名,而是指那個我們稱之為模板方法模式的方法或函式)的定義在基類中(有時作為一個保護或私有成員函式)並且不能改動——模板方法模式就是“堅持相同的程式碼”。它呼叫其它基類函式(就是那些被派生類覆蓋的函式)以便完成其工作,但是客戶程式設計師不必直接呼叫這些函式。

    我們先提供一個簡單的例子,然後再到一些常見的應用程式開發框架中找一些現成的例子來給大家參考。

class IUIElement {
protected:
virtual void drawBackground(Painter * painter) = 0;
virtual void drawElement(Painter * painter) = 0;
virtual void drawForeground(Painter * painter) = 0;
public:
void draw(Painter * painter)
{
drawBackground(painter);
drawElement(painter);
drawForeground(painter);
}
};
class CButton : public IUIElement {
protected:
void drawBackground(Painter * painter)
{
//draw background
}
void drawElement(Painter * painter)
{
//draw icon
//draw text
}
void drawForeground(Painter * painter)
{
//draw foreground if necessary
}
};

    上面的程式碼非常簡單,我們在日常的開發中經常會實現類似的類,其中 IUIElement 的 draw() 方法就是模板方法,它呼叫 drawBackground 、 drawElement 、 drawForeground 來完成 element 的繪製。

    在我使用過的一些開發框架中,有很多模板方法模式應用的例子,舉幾個來看看。

    先說 Qt ,它是一個完全用 C 實現的應用程式框架,目前可以在 Windows 、 Linux 、Android 、iOS、Mac 、塞班等各種平臺上使用。QWidget 類是 Qt 中所有可見控制元件類的基類。它使用了模板方法模式,我們在使用 QWidget 、QLabel 、QListView 等等這些類時,可以過載 paintEvent 、keyPressEvent 等,但是完全不必關心我們過載的這些方法在何處、何時會被呼叫,而實際上他們是被 QWidget 中的模板方法(比如 render
)呼叫的。下面是一部分被 QWidget 模板方法呼叫的可以被我們過載的方法(摘自 Qt 原始碼):

protected:
// Event handlers
bool event(QEvent *);
virtual void mousePressEvent(QMouseEvent *);
virtual void mouseReleaseEvent(QMouseEvent *);
virtual void mouseDoubleClickEvent(QMouseEvent *);
virtual void mouseMoveEvent(QMouseEvent *);
#ifndef QT_NO_WHEELEVENT
virtual void wheelEvent(QWheelEvent *);
#endif
virtual void keyPressEvent(QKeyEvent *);
virtual void keyReleaseEvent(QKeyEvent *);
virtual void focusInEvent(QFocusEvent *);
virtual void focusOutEvent(QFocusEvent *);
virtual void enterEvent(QEvent *);
virtual void leaveEvent(QEvent *);
virtual void paintEvent(QPaintEvent *);

    我最早接觸的 UI 框架是 MFC / ATL ,然後是 WTL 。MFC 中也有大量的模板方法模式應用,不再詳述,這裡要提一下 WTL ,它裡面也用到了模板方法模式,而且是很特別的一種實現。

    用過 WTL 的開發者都知道這個框架大量使用模板,簡直非模板無以 WTL 。我說的 WTL 使用模板方法模式的特別之處就在這裡,它通過引入模板,減少了對繼承的使用頻度(我們知道有個問題叫作“脆弱的基類”問題)。比如 CMDIFrameWindowImpl 這個模板類,實現了 OnSize 方法,通過 MESSAGE_HANDLER 這個巨集和 WM_SIZE 訊息關聯起來,其實 MESSAGE_HANDLER 這個巨集已經引入了模板方法,只是我們看不到,有興趣的可以研究 MFC 的標頭檔案或 ATL
的標頭檔案。特別的地方,我們先看下面的程式碼然後再回過頭來分析。

	LRESULT OnSize(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
if(wParam != SIZE_MINIMIZED)
{
T* pT = static_cast<T*>(this);
pT->UpdateLayout();
}
// message must be handled, otherwise DefFrameProc would resize the client again
return 0;
}

    上面的程式碼演示的不是模板方法本身,而是被模板方法呼叫的被派生類覆蓋的方法。在 OnSize 中,又通過 static_cast 將 this 指標轉換為模板類 T 的指標,呼叫 T 的 UpdateLayout 方法來應對 SIZE 的變化。這樣通過傳入不同的 T 就可以在編譯時生成不同功能的多文件視窗。我們知道策略模式( Strategy ),是在執行時選擇演算法,而 WTL 則把模板方法模式和策略模式結合了起來,達到了一種很奇妙的效果:策略實際上是編譯時(請理解模板的用法)已經固定下來的,但我們寫程式碼時卻有種動態選擇的感覺。
    

    來看看 Android 上的應用開發,View 是所有可見控制元件的基類,如果我們要實現一個自定義的 View ,那麼 onDraw 、 onMeasure 、 onLayout  、 onKeyDown 等方法恐怕是要過載的。這裡面也是模板方法模式在起作用,可以去看 Android 應用框架的原始碼。

    模板方法模式存在一個基本的問題,就是脆弱的基類問題。比如 MFC ,每次新版本釋出後,你都有可能要調整你的程式去適應——因為雖然編譯通過,但程式的行為可能因為基類的變化而變得不符合預期。我們說組合優於繼承,就和這個問題相關。 java 中有介面( interface ),可以強制派生類實現每一個方法,C 中沒有介面,但有純虛擬函式可以模擬介面,我們要儘可能的使用介面、組合,而不是不假思索的使用繼承(實現繼承)。