作者:朱金灿
来源:http://blog.csdn.net/clever101
如何应用设计模式,是一个见仁见智的问题,可能也没有一定之规。以我的水平也谈得不一定好。前段时间重构了公司软件中的二维图形交互方面的代码,总结了一些经验供大家分享,期待起一个抛砖引玉的作用。
该软件是基于MFC界面框架开发的。MFC的Doc-View架构本质上是一种MVC架构。随着软件功能开发越来越多,图形的二维显示及各种浏览命令、编辑命令的实现集中在视图类(即View)。视图类的代码越来用臃肿(将近上万行的代码),它的维护更新和升级以后都将大成问题。一个例证就是在各种键盘鼠标消息响应函数写无数的case分支语句来处理各种命令,类似于:
void CMyView::OnLButtonDown() { switch(m_Cmd) { case Cmd_Browse: { …… break; } case Cmd_Draw_Line: { …… break; } default: break; } }
毫无疑问,视图类已经趋向于成为一个上帝类(注意这里说的上帝类不是无所不能的类,而是只有上帝才懂的类)。
我已经闻到了一股代码发臭的味道.如何改造上帝类,成为我面临的一个问题。改造上帝类,不是简单的拆分代码那么简单。以前我看过一个系统的代码,里面视图类的代码也很多,然后就将视图类的一个cpp文件分为两个cpp文件。这就是一个简单拆分的例子,却无助于提升代码的可维护性和系统的可扩展性。
恰好在这个时候我阅读了微软的VS的画图示例drawcli的代码。这个示例给了我启发。在下面我将介绍drawcli的做法以及在学习它的基础上我的做法。
Drawcli的功能简单,主要是实现一些简单图形的绘制和编辑,如下图:
(红色画框为它的画图工具栏)
那么Drawcli是如何设计和实现它的绘图功能呢?视图类为CDrawView,我们看它是如何响应各种鼠标消息的:
void CDrawView::OnLButtonDown(UINT nFlags, CPoint point) { if (!m_bActive) return; CDrawTool* pTool = CDrawTool::FindTool(CDrawTool::c_drawShape); if (pTool != NULL) pTool->OnLButtonDown(this, nFlags, point); } void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) { if (!m_bActive) return; CDrawTool* pTool = CDrawTool::FindTool(CDrawTool::c_drawShape); if (pTool != NULL) pTool->OnLButtonUp(this, nFlags, point); } void CDrawView::OnMouseMove(UINT nFlags, CPoint point) { if (!m_bActive) return; CDrawTool* pTool = CDrawTool::FindTool(CDrawTool::c_drawShape); if (pTool != NULL) pTool->OnMouseMove(this, nFlags, point); }
我们看到CDrawView的鼠标消息响应函数中并没有具体实现,它是通过查找一个工具类CDrawTool,然后调用它的成员函数来实现的功能。我们再看看CDrawTool的代码:
// 图形类型枚举变量 enum DrawShape { selection, line, rect, roundRect, ellipse, poly }; // 绘制工具基类 class CDrawTool { // Constructors public: CDrawTool(DrawShape nDrawShape); // Overridables virtual void OnLButtonDown(CDrawView* pView, UINT nFlags, const CPoint& point); virtual void OnLButtonDblClk(CDrawView* pView, UINT nFlags, const CPoint& point); virtual void OnLButtonUp(CDrawView* pView, UINT nFlags, const CPoint& point); virtual void OnMouseMove(CDrawView* pView, UINT nFlags, const CPoint& point); virtual void OnEditProperties(CDrawView* pView); virtual void OnCancel(); // Attributes DrawShape m_drawShape; static CDrawTool* FindTool(DrawShape drawShape); static CPtrList c_tools; static CPoint c_down; static UINT c_nDownFlags; static CPoint c_last; static DrawShape c_drawShape; }; // 绘制矩形类 class CRectTool : public CDrawTool { // Constructors public: CRectTool(DrawShape drawShape); // Implementation virtual void OnLButtonDown(CDrawView* pView, UINT nFlags, const CPoint& point); virtual void OnLButtonDblClk(CDrawView* pView, UINT nFlags, const CPoint& point); virtual void OnLButtonUp(CDrawView* pView, UINT nFlags, const CPoint& point); virtual void OnMouseMove(CDrawView* pView, UINT nFlags, const CPoint& point); };
限于篇幅,我不一一列举这些类的实现代码。这里简单提提视图类是如何找到绘图工具类的,它是由CDrawTool类的静态函数FindTool实现的:
CDrawTool* CDrawTool::FindTool(DrawShape drawShape) { POSITION pos = c_tools.GetHeadPosition(); while (pos != NULL) { CDrawTool* pTool = (CDrawTool*)c_tools.GetNext(pos); if (pTool->m_drawShape == drawShape) return pTool; } return NULL; }
我将这个做法称之为操作面向对象化。我们看以前的C++教材,在介绍面向对象时,往往举这样的例子:如class person,就是说现实世界中的事物都可以作为对象来看待。这样的说法通俗易懂,也不能说不对。但拘泥于这样的认识,往往是将面向对象之对象仅限于此。其实不然。事物之间的联系也可以面向对象化,而且在设计复杂的系统时,这种面向对象显得更为重要。
将操作动作面向对象化,在成熟的软件框架并不鲜见。做过ArcGis二次开发的朋友都知道,ArcGis中的SDK中有两个虚接口:ITool和ICommand。这二者其实就是将操作面向对象化。ICommand和ITool的区别在于:ICommand不需要用鼠标等与地图交互,如全图功能,ITool则需要,如选择功能。你也可以想到Drawcli中的CDrawTool和ITool的功能是一样的。
到此时可能你会问我:你所讲的和你的题目是什么关系?下面我要说的正是这个问题的答案。Drawcli的设计正是标题中的Comand模式的一个具体体现。让我们重温一下Comand模式的应用场景:许多系统都会收到,发送并处理请求。条件调度程序是一条条语句(比如switch语句),它用来执行请求的发送和处理,有些简单情况适合它们,复杂的情况下就不适合。这种调度程序的代码如果在一页显示,还可以,但是负责情况下:
- 缺少足够的运行时灵活性
- 代码的膨胀
Comand的解决方案是这种问题最好的方案。只需简单地把每块处理逻辑放到一个单独的“命令”类中,这个类有一些通用的方法,如execute(),run(),用来执行它所封装的处理逻辑。一旦有了这样一批命令类,就可以用一个集合来存储,获取它们的示例(添加,删除,修改),并通过它们的执行方法执行这些示例。
在Drawcli例子的启发下,我这样重构我的代码。我也像ArcGis SDK那样设计,将用户的操作分为两类:需要和视图进行鼠标交互的工具类和不需要和视图进行交互的命令类。大致的代码如下:
enum cmd_type { Cmd_NoOpertin, // 无操作 Cmd_ViewAll, // 全图显示 … }; // 命令类基类 class CBaseComand { public: CBaseComand(CMyView *pView); virtual ~ CBaseComand(); virtual void Execute(); // 外部调用方法 // private: CMyView *pView; // 保存视图类指针,方便访问视图类的数据 } class CViewAllCmd : public CBaseComand { public: CViewAllCmd(CMyView *pView); virtual ~ CViewAllCmd (); void Execute(); // 外部调用方法 // private: CMyView *pView; // 保存视图类指针,方便访问视图类的数据 }
在如何创建命令方面我并没有采用drawcli的做法(drawcli的做法是采用一个链表将各种工具保存下来)。我应用了工厂模式,即定义了一个CCmdFactory类专门用于创建命令,该类只有一个静态方法:
CBaseComand* CCmdFactory::CreateCmd(CMyView *pView,cmd_type type) { switch(type) { case Cmd_ViewAll: return new CViewAllCmd(pView); …… } }
当需要执行某一操作时,就销毁前一个命令,然后新建一个操作命令。工具类的设计与命令类类似,不同的是工具类有响应鼠标消息的接口。所以在此不进行赘述。
实践证明,这次重构效果良好,具体表现在该模块的可扩展性和可维护性大为提高。这次重构的经验可以简单概括为:在重构之前我没想过要用什么设计模式,我分析代码中现存的问题及潜在的问题(重点在可维护性、可扩展性方面衡量),然后参考成熟的开源代码的做法,最后再结合所学的设计模式的理论深化自己的认识。