【原创&交流】Comamnd模式和Factory模式在一次代码重构中的应用
如何应用设计模式,是一个见仁见智的问题,可能也没有一定之规。以我的水平也谈得不一定好。前段时间重构了公司软件中的二维图形交互方面的代码,总结了一些经验供大家分享,期待起一个抛砖引玉的作用。
该软件是基于MFC界面框架开发的。MFC的Doc-View架构本质上是一种MVC架构。随着软件功能开发越来越多,图形的二维显示及各种浏览命令、编辑命令的实现集中在视图类(即View)。视图类的代码越来用臃肿(将近上万行的代码),它的维护更新和升级以后都将大成问题。一个例证就是在各种键盘鼠标消息响应函数写无数的case分支语句来处理各种命令,类似于:
(红色画框为它的画图工具栏)
那么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类专门用于创建命令,该类只有一个静态方法:
[解决办法]
比较实用啊,学到知识了。
[解决办法]
"在如何创建命令方面我并没有采用drawcli的做法(drawcli的做法是采用一个链表将各种工具保存下来)。我应用了工厂模式,即定义了一个CCmdFactory类专门用于创建命令,该类只有一个静态方法:"
drawcli的方法挺好,command一般都是没有状态的执行者,创建一次就好了,不用每次都new
[解决办法]
建议楼主考虑一个策略模式先。
另外设计模式一个重要的思想就是对修改封闭,对扩展开放。那如果你要增加新的command,那你的command factory是不是就要重新修改了?
[解决办法]
可以结合state模式看一下