命令模式在数据库应用程序中的应用






4.70/5 (16投票s)
命令模式应用于数据库应用程序的实际示例

引言
在现代编程世界中,“命令模式”是最常被提及的词汇之一,它在实现世界级应用程序中的撤销/重做功能方面几乎获得了奥斯卡奖。然而,这个设计模式的实际应用却非常罕见,除非你运气极好(至少我没那么幸运)。有大量的论文、书籍或网站都在讨论这个模式的理论方面,但没有一个提供具体实践案例。所以,本文将演示我在数据库应用中自己实现命令模式的实践。
背景
在我之前的项目中,我曾需要实现一个遵循命令模式的命令处理器,以支持多个操作的撤销/重做。后来,在我一位同事的项目(一个数据库项目)中,需要实现工作流序列化,在执行了多个数据库操作后,需要回滚一些步骤并从之前的某个点重新开始。这给了我一个想法,将命令模式应用于数据库应用,并观察其回滚和重做的特殊效果。当然,我并没有将我的“天才代码”应用于那个真实的数据库项目,因为我只做了小范围的实现,而原始应用的上下文不同且规模庞大。请注意,这个概念仍然可以应用于大型应用。
命令处理器类
要实现命令模式,首先需要一个命令处理器类。这个类将负责管理命令,并处理命令的执行和回滚。
为了封装一个命令,我创建了一个有用的 `abstract` 类,所有的应用特定命令都将从中派生。我将这个基类命名为 `CGCommand` 类,其代码如下:
// CGCommand class
class CGCommand
{
public:
CGCommand(){ }
virtual ~CGCommand() { }
// Override this to perform a command
//
// Returns TRUE if successful, otherwise FALSE
virtual BOOL Execute() = 0;
// Override this to undo a command
//
// Returns TRUE if successful, otherwise FALSE
virtual BOOL UnExecute() = 0;
// Override this to determine whether a command can be undo
//
// Returns TRUE if successful, otherwise FALSE
virtual BOOL CanUndo()
{
return TRUE;
}
// Override this to determine whether a command can be redo
//
// Returns TRUE if successful, otherwise FALSE
virtual BOOL CanRedo()
{
return TRUE;
}
// Override this to serialize a command
// (Not implemented in the command processor level yet)
//
// Returns TRUE if successful, otherwise FALSE
virtual BOOL Load()
{
return FALSE;
}
// Override this to deserialize a command
// (not implemented in the command processor level yet)
//
// Returns TRUE if successful, otherwise FALSE
virtual BOOL Save()
{
return FALSE;
}
};
我希望注释能够解释清楚,这个类也非常简单易懂。现在我让你了解了命令类的样子,让我们来看看核心类——*命令处理器*以及它的 API。
// Command Processor is the Owner of Commands,
// therefore it is responsible for clearing the memory allocated for commands
class CGCommandProcessor
{
public:
CGCommandProcessor();
virtual ~CGCommandProcessor();
// if bStore is true then the command is executed and also
// saved in the command chain for undoing purposes
// if bReleaseIfNotStored is TRUE then the command is deleted from memory
// when it is not stored
//(due to command execution failure or bStore set as FALSE)
virtual BOOL Submit(CGCommand* pCommand, BOOL bStore = TRUE,
BOOL bReleaseIfNotStored = TRUE);
// Store a Command in the command chain without executing it
virtual BOOL Store(CGCommand* pCommand);
// Retrieve the current command
//
// Returns the current command
CGCommand* GetCurrentCommand();
// Retrieve the next command
//
// Returns the next command
CGCommand* GetNextCommand();
// Determine whether a command can be undo
//
// Returns TRUE if successful, otherwise FALSE
virtual BOOL CanUndo();
// Determine whether a command can be redo
//
// Returns TRUE if successful, otherwise FALSE
virtual BOOL CanRedo();
// Execute the undo operations
//
// Returns TRUE if successful, otherwise FALSE
virtual BOOL Undo();
// Execute the redo operations
//
// Returns TRUE if successful, otherwise FALSE
virtual BOOL Redo();
// Set the size of command array
//
// nMaxNoCommands : Specifies the array size
//
void SetCommandsArrayMaxSize(UINT_PTR nMaxNoCommands)
{
m_nMaxNoCommands = nMaxNoCommands;
}
// Retrieve the size of command array
//
// Returns the size of command array
//
UINT_PTR GetCommandsArrayMaxSize()
{
return m_nMaxNoCommands;
}
// No Bounds Checking for Clear Commands, so use at your own risk
void ClearCommands();
// When ClearCommands is invoked, the CurrentCommandIndex position
//is set to nStartIndex - 1
void ClearCommands(INT_PTR nStartIndex, INT_PTR nCount = 1);
....
....
};
所以,你大概清楚了命令处理器做什么。它通过 `Submit` API 接收命令,执行它,并(如果指定)存储它。然后你可以调用 `undo` 和 `redo` API,最终会调用命令对象的 `UnExecute` 和 `Execute` API。处理器会像你期望的那样,维护命令链中的当前命令位置及其执行和未执行状态。现在一个实际的例子应该能更充分地阐明命令类及其处理器的用法。
数据库命令
正如我之前提到的,要封装一个应用命令,你需要实现 `abstract` 类 `CGCommand` 并主要重写 `Execute` 和 `UnExecute` API。在我的数据库应用中,主要有三个操作:添加、删除和编辑行。虽然我只处理单行添加、删除和编辑,但这个概念可以轻松扩展到处理多行。
// CGDBAddRowCommand class
class CGDBAddRowCommand : public CGDataBaseCommand
{
public:
CGDBAddRowCommand(_RecordsetPtr pSet, const CDDXFields& fields, CDataGrid* pGrid);
// Perform a command
//
// Returns TRUE if successful, otherwise FALSE
BOOL Execute();
// Undo a command
//
// Returns TRUE if successful, otherwise FALSE
BOOL UnExecute();
CDataGrid* m_pGrid;
};
// CGDBDeleteRowCommand class
// CGDBDeleteRowCommand is nothing but CGDBAddRowCommand with reverse action
class CGDBDeleteRowCommand : public CGDBAddRowCommand
{
public:
CGDBDeleteRowCommand(_RecordsetPtr pSet, const CDDXFields& fields, CDataGrid* pGrid);
// Perform a command
//
// Returns TRUE if successful, otherwise FALSE
BOOL Execute();
// Undo a command
//
// Returns TRUE if successful, otherwise FALSE
BOOL UnExecute();
protected:
BOOL m_bFirstTimeExecution;
};
// CGDBEditRowCommand class
class CGDBEditRowCommand : public CGDataBaseCommand
{
public:
CGDBEditRowCommand(_RecordsetPtr pSet, const CDDXFields& fields, CDataGrid* pGrid);
// Perform a command
//
// Returns TRUE if successful, otherwise FALSE
BOOL Execute();
// Undo a command
//
// Returns TRUE if successful, otherwise FALSE
BOOL UnExecute();
CDDXFields m_undoDDXFields;
CDataGrid* m_pGrid;
};
以上所有类都继承自 `CGDataBaseCommand` 类,它只是 `CGCommand` 类的派生类,并包含一些额外的属性。以下是该类的声明:
class CGDataBaseCommand : public CGCommand
{
public:
CGDataBaseCommand(_RecordsetPtr pSet, const CDDXFields& fields)
{
m_pSet = pSet;
m_DDXFields.RemoveAll();
m_DDXFields.Append(fields);
}
protected:
_RecordsetPtr m_pSet;
CDDXFields m_DDXFields;
};
如果你查看以上所有类的实现,你会发现在 `CGDBAddRowCommand` 类的 `Execute` 方法中,它使用记录集指针 `m_pSet` 向数据表中添加一行。然后,它将新创建的行的书签保存在一个概念性映射 `m_mapBookMarksVsCommands` 中(实际上是通过 MFC 的 `CArray` 实现的)。接着,当调用 `UnExecute` 方法时,它会从该映射中检索行的书签(检索书签的键是命令对象本身),并删除与该书签对应的行。有一个小技巧是,在调用 `UnExecute` 之后,会调用 `UpdateBookMarks` 函数,然后再次调用 `Execute` 来重新添加刚才删除的行(简而言之,就是撤销后的重做)。`UpdateBookMarks` 函数的作用是,找到之前在调用 `Execute` 时保存的行的书签,并用重新调用 `Execute` 后创建的新行书签替换它。我们需要这样做,因为在执行撤销 (`UnExecute`) 后,初始书签将成为一个悬空指针。好了,关于添加行就说到这里。我们继续讨论删除行。
`CGDBDeleteRowCommand` 类只是 `CGDBAddRowCommand` 类的反向版本,其实现与后者正好相反,如果你浏览这个类的源代码就能清楚地看到这一点。
最后,`CGDBEditRowCommand` 类在 `Execute` 方法中使用成员记录集指针来编辑一行,同时保存该行的先前信息,以便进行撤销编辑。当调用 `UnExecute` 时,它只需要将该行编辑回这些保存的值。该类还在 `Execute` 中保存了行的书签,以便在执行撤销时能够回到确切的行。
本文的真实意图是让你感受如何封装一个命令以实现撤销和重做功能。尽管我实现的这些操作很简单,但聪明的读者在数据库应用中,可以利用记录集(或者在 C# 等更高级的语言中使用类似但更友好的东西来替代记录集)来实现更高级的功能。
Using the Code
以下是命令处理器声明和获取其引用的示例(一个获取命令处理器单例实例的有用函数):
CGCommandProcessor& AppGetCommandProcessor()
{
static CGCommandProcessor g_commandProcessor;
return g_commandProcessor;
}
提交、执行和存储命令的示例
if(bAddData)
{
AppGetCommandProcessor().Submit( new CGDBAddRowCommand
(m_pSet, m_DDXFields, m_pGrid) );
}
else
{
AppGetCommandProcessor().Submit( new CGDBEditRowCommand
(m_pSet, m_DDXFields, m_pGrid) );
}
关注点
在删除最后一行后,记录集指针可能指向 EOF,所以你需要额外调用一次 `MovePrevious` — 一个记录集指针 API,使其指向一个有效行。此应用并未在完全为空的数据表上进行测试,所以请注意可能出现的崩溃。如果确实发生崩溃,我希望有人能好心在 `BOOL CGDBAddRowCommand::UnExecute() ` 方法中添加适当的检查,并通知我解决方案。提前感谢。
致谢
我的感谢送给 Ariful Huq 先生(Enosis Solutions),他分享了他的一些项目细节,这给了我将命令模式概念应用于数据库领域的灵感。
我衷心感谢 Kirill Panov (https://codeproject.org.cn/script/Membership/View.aspx?mid=30110),感谢他高水平的 `CDataGrid` 实现以及相关应用(https://codeproject.org.cn/KB/miscctrl/datagrid.aspx),这使得我在数据库应用中应用命令模式概念变得非常容易。
历史
- 文章上传日期:2011年2月8日