无处不在的模板方法模式
话说网上总结的设计模式都以单例,工厂,观察者等模式最多,但是我个人觉得真正无处不在的却是模板方法。曾经有一位微软的讲师说过如果你只想学一种设计模式,那就模板方法吧。
笔者曾经开发过一款安全软件,其中负责云扫描模块。当然扫描部分也不止只用在云查杀部分,例如附带的清理功能,保险箱扫描可保护的软件都会用到扫描文件功能。但是如果分开写几套扫描逻辑却实在不可取,于是当时我用了下面的设计:
class IScanBase { public: BOOL ScanFile(CString strPath) { //省略扫描逻辑,调用GetFileInfo判断 GetFileInfo(strPath); } protected: BOOL GetFileInfo(CString strFilePath) = 0; }; class CCloudScan { protected: BOOL GetFileInfo(CString strFilePath) { //省略判断是否木马 } };
这样设计的好处就是查杀模块以及会扩展的保险箱模块,清理模块共用一套扫描机制,扫描逻辑存在于父类当中,而子类可以根据不同的需求修改接口逻辑从而实现不同的功能,这就是模板方法模式的应用。模板方法定义如下:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。上述代码中算法骨架就是扫描逻辑,而延生到子类中的就是根据不同需要针对文件处理的逻辑,通过上述设计即使是以后如果要新增需求也只需要增加相应的类即可。
其实这种设计在很多框架中都有用到,下面举几个例子说明:
MFC中的模板方法:
1、CView类中的virtual void OnDraw(CDC* pDC) = 0;接口就是明显的模板方法模式,该接口在void CView::OnPaint()中被调用。而Coder只用关系如何修改OnDraw而压根不用去关注该接口何时或者在什么时机被调用,该设计方法与上述测试代码如出一辙。
2、实现过MFC自绘控件的朋友都知道void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);这个函数往往是从void CWnd::OnDrawItem(int /*nIDCtl*/, LPDRAWITEMSTRUCT lpDrawItemStruct)调用过来,而最早也是为了响应WM_DRAWITEM。这一套消息流程源码清晰可见,本人便不再多讲。但是用设计来说,这也是典型的模板方法模式的应用。比如说要自绘一个按钮,Coder只需要重新实现DrawItem即可,至于他怎么被调用或者何时被调用压根不用关心。
3、改变MFC消息控制流的函数virtual BOOL PreTranslateMessage(MSG* pMsg);该函数是消息在到TranslateMessage之前被调用,通过重载这个函数我们可以改变MFC的消息流程。事实上该函数也是CWnd中的虚函数,如果我们做改写也可以达到我们自己的目的如拦截XX消息之类。细看该实现过程其实也是类似但并不完全是模板方法模式。
Duilib中的模板方法: 在开源的DuiLib中有一个这样的函数:
void CControlUI::DoPaint(HDC hDC, const RECT& rcPaint) { if( !::IntersectRect(&m_rcPaint, &rcPaint, &m_rcItem) ) return; // 绘制循序:背景颜色->背景图->状态图->文本 PaintBkColor(hDC); PaintBkImage(hDC); PaintStatusImage(hDC); PaintText(hDC); PaintBorder(hDC); }
其实被调用的Paint系列函数全都是CControlUI内部的虚函数,当CControlUI的子类也就是虚拟控件类需要实现自己的界面显示时只需要重写上面的Paint系列函数即可,至于何时调用不用关心。其实这也是典型的模板方法模式的应用。
事实上MFC,DuiLib中用到的模板方法还不止这些,例如duilib里面获取xml文件名可以用到的方法,MFC中的DoDataExchange函数等,如果仔细看来还能挖掘。但是对于我们来说不管体会才能更深的领悟设计模式的精髓。