在前面的文章中我们比较过框架和设计模式,一般的应用程序框架中都会大量用到设计模式。应用程序开发框架允许从一个或一组类中继承以便创建一个新的应用程序,重用现存类中几乎所有的代码,并且覆盖其中一个或多个函数以便自定义所需要的应用程序。
应用程序开发框架中的一个基本的概念是模板方法( 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 *);
用过 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++ 中没有接口,但有纯虚函数可以模拟接口,我们要尽可能的使用接口、组合,而不是不假思索的使用继承(实现继承)。