• Undo/Redo框架(C++,带源码)


     

    目录

     

    前言

    框架设计

    代码实现

    单元测试

    后记

    参考资料

     

    前言

     

    终于结束赋闲在家的状态,又走上研发经理的岗位。老板“教导”我说:“作为‘空降’的管理者,要想得到团队中其他成员的信任和认可,必须身先士卒,去解决开发中难题。”言下之意很明显,得先干Hands-on的工作。于是我便有了做现有系统图形操作的撤销和恢复(Undo/Redo)功能的任务,因为这项工作被其他人认为是比较难啃的骨头(原因是你要在现有功能的实现代码中加入这个 Undo/Redo,而这些代码是由多人写的,要读懂它们就得费不少功夫,最多的一个操作2000多行代码,还不算间接调用的函数)。

     

    当然,实现具体功能的Undo/Redo之前,首先要搭建一套Undo/Redo的框架。不过好在实现Undo/Redo框架还不是那么复杂。以前看到过一些关于如何实现Undo/Redo功能的书和网页,带过的团队也曾做过Undo/Redo,但是自己亲自下手,还是头一回。下面就把设计、实现和测试的过程回顾一下,算是做个总结。

     

    框架设计

     

    一、最基本的,当然是使用命令(Command)设计模式。见下面的类图:

     

    clip_image002

     

    如果用C#,可能用接口(interface)来定义它们比较好,比如定义ICommandICommandManager。但C++中没有interface,所以用抽象类(Abstract Class)来实现,所有方法都声明为纯虚函数。

     

    至于Command设计模式,无需多说,无非这里的Command 模式带Undo/Redo功能。

     

    二、下一步当然是BaseCommandManager的实现子类CommandManager,见下面的类图:

     

    clip_image004

     

    CommandManager内部会维护着2个栈(Undo StackRedo Stack),并增加相应的操作栈的私有方法,如:PushUndoCommandPopUndoCommand等。CommandManager基于这个数据结构来实现BaseCommandManager声明的所有纯虚函数。

     

    三、下面是BaseCommand的实现子类Command。根据《Head First设计模式》里说得,这里有两种方案,一种是“傻瓜式”的Command,即Command持有一个接收者(Receiver)的指针,所有具体的命令都由Receiver来处理,Command对如何处理命令一无所知,像“傻瓜”一样。另一种是“聪明”的CommandBaseCommand的子类知道如何处理命令并直接处理。这里我采用了“傻瓜式”的Command(当然此框架也支持“聪明”的Command,但Command子类需要根据客户程序的需要由框架的使用者自己来实现了),见下面类图:

     

    clip_image006

     

    BaseCommandReceiver也是个抽象类,声明一个纯虚函数ActionCommandExecuteUnexecute方法分别用falsetrue作为参数调用虚方法Action以执行命令。

     

    客户程序在生成Command的同时,应该给其赋予一个BaseCommandReceiver的指针m_pReceiver。缺省地,Command销毁的时候会一并销毁m_pReceiver,但客户程序也可通过设置bAutoDeletefalse,来自己销毁。

     

    BaseCommandReceiver的子类负责实际的命令处理。

     

    四、很多书中介绍Command设计模式的时候,都会提到组合命令。用组合命令可以实现命令的“批处理”。我们这里也需要,所以要实现BaseCommand的另一个子类MacroCommand。见下面类图:

     

    clip_image008

     

    MacroCommand维护一个Command的集合(这里用std::vector实现),客户程序可以添加命令(AddCommand)和删除命令(DeleteCommand)。当然MacroCommand也要实现BaseCommandExecuteUnexecute函数,具体的实现就是遍历vector中的Command,逐个执行和逐个撤销。

     

    五、支持Undo/Redo的应用程序,一般都有“Undo”和“Redo”两个按钮,那么当命令的Undo/Redo栈内容变化的时候,两个按钮会根据是否“可撤销”和“可恢复”相应地变化为EnabledDisabled状态。那么框架支持这个功能是通过观察者(Observer)设计模式来完成的。Observer模式也无需多说,见下面的类图:

     

    clip_image010

     

    简单的说,当CommandManagerUndoRedo栈由空变为不空时,或由不空变为空时,都会调用SubjectNotify函数通知所有观察者,来更新UI(比如:Undo/Redo按钮的Enabled/Disabled状态)。

     

    六、这个框架支持的五个基本操作:执行命令、UndoRedo、清除Undo/Redo历史记录、Undo/Redo状态改变的时序图依次如下:

     

    clip_image012 clip_image014 clip_image016 clip_image018clip_image020

     

    代码实现

     

    BaseCommandManager.h

    BaseCommand.h

     

    在使用此框架时,可能会产生一些BaseCommand的子类。这里使用对象工厂(Object Factory)设计模式来实现BaseCommand子类的创建,目的是解除这些子类与框架的耦合。(关于对象工厂,参见本人另一篇博客《对象工厂设计模式》。Factory.h的代码见下)

     

    Factory.h

     

    所以在BaseCommand中定义RegisterCommandClass模板类来支持BaseCommand子类向工厂的注册。并且在BaseCommand类里增加CreateCommand静态函数(包括宏定义CREATECOMMAND),通过工厂在运行时可以“动态”生成BaseCommand的子类。

     

    CommandManager.h

    CommandManager.cpp

     

    CommandManager实现为单件(这不是必须的,只不过我们的系统需要这样做)。这个单件由Singleton模板类实现(Singleton<CommandManager>::Instance())。顺便说一句,把Singleton做成模板类的好处是:单件有许多变种(Mayers单件、Phoenix单件、带寿命的单件和双检测锁定单件等),当需要修改单件的实现方法时,只需改这个模板类即可,不用每个单件类都去修改。Singleton模板类代码如下(这里Singleton不是线程安全的,需要加双检测锁定才能支持多线程):

     

    Singleton.h

     

    CommandManager里有个内嵌类UndoRedoStateInspector,它的作用相当于一个“门卫”,“守卫”在CommandManagerCallCommandClearAllCommandsUndoRedo函数的“门口”,它可以在进入函数时(即构造UndoRedoStateInspector时)保存CanUndoCanRedo的状态,当退出函数时(即析构UndoRedoStateInspector时),检查2个状态是否改变,如改变则通知观察者(Observer)们状态已改变。这个做法非常类似于多线程编程中经常使用的Lock对象(即在构造时获得Mutex,析构时释放Mutex)。

     

    CommandManager的最后还定义了几个宏CALLCOMMANDUNDOREDO,目的是方便调用(可以使调用者少敲一些字符)。

     

    Command.h

    Command.cpp

     

    作为BaseCommand的子类,Command向工厂注册自己。

     

    RegisterCommandClass<Command> RegisterCommandClass(ClassNameToString(Command));

     

    在客户程序创建Command对象时,代码可能像下面这个样子:

     

    Command * pCommand = (Command *)CREATECOMMAND(Command);

     

    MacroCommand.h

    MacroCommand.cpp

    Util.h

     

    同样,作为BaseCommand的子类,MacroCommand也需要向工厂注册自己。

     

    RegisterCommandClass<MacroCommand> RegisterCommandClass(ClassNameToString(MacroCommand));

     

    MacroCommand的析构函数要清理m_vecCommands,所以写了个ContainerDeleter。这个模板函数其实可以胜任任何支持迭代器的容器的清理工作(delete元素和clear容器)。

     

    BaseCommandReceiver.h

     

    在使用此框架时,可能会产生一些BaseCommandReceiver的子类。所以与BaseCommand类似,BaseCommandReceiver也使用对象工厂(Object Factory)设计模式来实现子类的创建。BaseCommandReceiver中定义RegisterCommandReceiverClass模板类来支持BaseCommandReceiver子类向工厂的注册。并且在BaseCommandReceiver类里增加CreateCommandReceiver静态函数(包括宏定义CREATECOMMANDRECEIVER),通过工厂在运行时可以“动态”生成BaseCommandReceiver的子类。

     

    最后是观察者设计模式(SubjectObserver)的代码,见下:

     

    Subject.h

    Subject.cpp

    Observer.h

    Observer.cpp

     

    SubjectObserver都不是线程安全的,如果要支持多线程, SubjectObserver的函数都要加互斥体(Mutex)。

     

    单元测试

     

    这里使用Google Test作为单元测试的框架。(说明一下:以下的单元测试并没有对每个单独的类做单元测试,只是对整个框架做单元测试。)

     

    使用一个测试装置(Test Fixture)来测试:声明一个Invoker类,维护一个元素为int型的list,并负责压入和弹出元素、清除list、显示list、“观察”Undo/Redo状态变化等工作。我们的测试将对这个list以及对其追加数据的命令(见下面MockCommandReceiver)而展开。

     

    Invoker.h

    Invoker.cpp

     

    声明MockCommandReceiver来实现对list追加数据的操作。MockCommandReceiverBaseCommandReceiver 的子类。

     

    MockCommandReceiver.h

    MockCommandReceiver.cpp

     

    下面的测试用例中,有个对命令执行失败情况的测试,所以声明MockCommand来模拟执行成功和失败。

     

    MockCommand.h

    MockCommand.cpp

     

    要测试的内容包括:

     

    1.       简单命令的调用、撤销和恢复

    2.       组合命令的调用、撤销和恢复

    3.       清除所有命令

    4.       在撤销一个命令后调用另一个命令

    5.       失败的命令调用、撤销和恢复

    6.       大量的命令调用、撤销和恢复

    7.       以上操作后,Undoable/Redoable的状态

     

    每个用例的目的、步骤和期望结果就不赘述了,看代码吧。

     

    TEST_F(Invoker, TestUndoRedoFramework)

     

    后记

     

    有人说:“你罗罗嗦嗦地说这么多,不就是个Undo/Redo框架么,至于这么费劲么?”不错,说得确实有点罗嗦。不过,在实际的工作中,对以上每一个技术细节的思考都是不可缺少的。当你的代码将被别人使用的时候,多费点精力在稳定性、可复用性、可扩展性等方面,还是很值得的。

     

    以上内容,如有谬误,敬请指出,先谢过了!

     

    请点击此处下载源代码

     

    参考资料

     

    《设计模式 - 可复用面向对象软件的基础》5.2 Command(命令)- 对象行为型模式

    Head First设计模式》6 封装调用:命令模式

    《敏捷软件开发 - 原则、模式与实践(C#版)》第21 COMMAND模式

    C++设计新思维》部分章节

    Getting started with Google C++ Testing Framework

     

  • 相关阅读:
    ubuntu 上安装ssh
    应用架构设计原则、模式摘录
    经典算法摘录
    CSS3选择器笔记
    微信支付 统一下单 字段 body 为中文时 报【签名错误】解决方案(C# SDK)
    C#获取gif帧数
    C#根据byte前两位获取图片扩展名
    使用Amazon AWS SNS 发送 SMS 消息 .net
    MyCAT全局序列号-数据库方式
    MyCAT入门实践
  • 原文地址:https://www.cnblogs.com/wanghui9072229/p/2158960.html
Copyright © 2020-2023  润新知