在许多编辑器中,大都实现了Undo / Redo操作。从界面出发,Undo/Redo仅仅只是一种抽象的”提供单一接口行为“的操作,尽管实际的Undo/Redo工作可能会涉及各种不同的具体操作。举个例子,在3dMax这样的软件中,我们可以创建3D实体,可以编辑其自身的各种属性(譬如立方体有长宽高,球有半径),可以移动实体的位置,对其做旋转,缩放操作,乃至对个别的网格顶点,面片的细微调节。其中的每一步,都可以撤销或重做。

我们仔细思考一下,就会明白,撤销和重做实际上是需要保存每一步操作的”被操作实体的两个状态“(分别是该实体在操作发生前和发生后的状态)。

譬如对一个物体做以移动,我们实际上是保存了移动前的位置,和移动后的位置,这样在Undo/Redo时,只需要分别将其置于所需的状态即可。

而不管具体的状态是如何转变的,对于用户来说,撤销/重做都只是一个简单的命令,表现为一个按钮,或者一次Ctrl+Z。这其实就是一种具有同一性表现的动作,在软件实现中,非常适合使用Command模式对其进行抽象。

 

譬如,我们只需要定义这样的一个基类即可明晰撤销/重做操作的概念接口:

class Command {

public:

         virtual ~ Command() = 0 {}

         virtual bool Do() = 0;

         virtual bool Undo() = 0;

bool Redo() { Do(); }

};

 

所有的Undo/Redo的状态,都派生自这个Command抽象类,它定义了两个纯虚函数,用于做和撤销。Redo函数只是简单的调用Do()实现其功能。

 

有了上面这个接口,当我们想添加更多类型操作的Undo/Redo支持的时候,只需要这样做:

class MoveBoxCommand{

public:

         MoveBoxCommand(VECTOR3 vNewPos, int boxid)

                   : m_iID(boxid), m_vNewPos(vNewPos) {}

         ~MoveBoxCommand() {}

bool Do()

{

m_vOldPos = SomeManager->GetObject(m_iID)->GetPos();

         return SomeManager->GetObject(m_iID)->SetPos(m_vNewPos);

}

bool Undo()

{

return SomeManager->GetObject(m_iID)->SetPos(m_vOldPos);

}

private:

int m_iID;

VECTOR3 m_vNewPos;

VECTOR3 m_vOldPos;

};

 

现在我们就可以对任意的对象支持Move操作的撤销/重做了,事实上,旋转,缩放等其他操作大都与此类似,对添加删除之类的操作需要做一点特别的处理(因为我们不能简单的把实际对象释放掉,否则其他引用了该对象的Command就会出现问题,一种良好的做法是设置删除标志)。

除此之外,我们需要将所有的操作产生的Command实例放在一个管理器中统一管理,假设其被命名为CommandManager。则CommandManager需要负责维护当前的Command,需要在用户调用Undo时,判断是否能够Undo,如果能够,则取出下一个Undo的Command,调用其Undo,并将当前Command的指针或者索引更新位置。

如果用户在某个点做出了新的操作,则需要将当前Command之后的所有Command清空。(如果用户没有做新操作,则可以使用当前Command之后的Command来实现Redo操作的,但是新操作会冲掉位于Redo序列中的元素)

如果撤销/重做的实现有了以上理解,就不难理解为什么几乎所有的软件在用户Undo多步,再采取新动作之后,就无法Redo了。

 

通过这种方式,我们就可以实现一个具有可扩展性的撤销重做机制了,不论再加入何种类型的新操作,我们都可以设法将其纳入到上述的框架当中。

 

具体的撤销重做实现还牵扯到许多细节,不过那都与主题关系不大,我们再来看一个例子:线程池。

 

有时我们可能会想编写一个线程池,用于处理各种并发的任务。线程池从概念上说是一个很简单的东西,有一个线程的集合,里面的很多线程都睡着,当有任务来的时候,一个线程跳出来把任务领走并执行,执行结束后,线程继续回到池子里睡觉。如果有多个任务,那么就会有许多个线程都跳出来,直到池子里没有更多的空闲线程或者任务队列被领空为止。

 

在Win32平台下编写多线程程序,大致会像这样:

void ThreadProc(PVOID param)

{

         // do something

}

 

// somewhere in some func, we create the thread

int handle = __beginthreadex(ThreadProc, valuePassin);

 

问题来了:

线程处理函数一般只有一个通用的模板,类似于上述的ThreadProc,线程池中的线程在创建的时候,需要传递一个处理函数地址,但是由于线程池中有许多线程,他们是批量被创建出来的,在创建这些线程的时候,我们无法立即获知所需处理的内容。因此,经济适用的方法是使用一个通用的线程处理函数,所有的线程被创建时传递的都是这个公共的处理函数。(而不是对每一个任务创建一个采用不同线程处理函数的新线程–这就不是线程池了,而是普通的多线程处理)

具体一点,一段提供如上功能的代码如下:(这个简陋的实现仅为演示上述思想,真正的实现需要对线程同步处理得更加规范,对错误意外情况做处理,完善其他必要的功能。P.s.这段代码是在word中编写,不能保证可以编译通过)

 

class ThreadPool;

void ThreadProc(PVOID param)

{

         ThreadPool * pThreadPool = reinterpret_cast< ThreadPool*>(param);

         ThreadTaskPool* pTaskPool = pThreadPool->GetTaskPool();

         while (true) {

         Task* pTask = pTaskPool->PopTask();

         If (!pTask)

                   Sleep(100);

         else

                   pTask->Do();

}

}

 

class Task {

public:

virtual void Do() = 0;

};

 

class ThreadTaskPool {

public:

ThreadTaskPool() { ::InitializeCriticalSection(&m_cs); }

Task* PopTask();

void PushTask(Task* pTask);

private:

CriticalSection m_cs;

std::queue<Task*> m_TaskQueue;

};

 

Task* ThreadTaskPool::PopTask()

{

         Lock<CriticalSection> _lock_;  // lock is a scope wrapper

         if (m_TaskQueue.empty())

                   return NULL;

         Task* pTask = m_TaskQueue.front();

         m_TaskQueue.pop_front();

         return pTask;

}

void ThreadTaskPool::PushTask(Task* pTask)

{

         Lock<CriticalSection> _lock_;

         m_TaskQueue.push_back(pTask);

         // we may do something else here(raise some event to notify the thread to wake up).

}

 

class ThreadPool {

public:

void Init();

ThreadTaskPool* GetTaskPool() const;

private:

HANDLE m_hThreads[MAX_THREADS];

ThreadTaskPool* m_pTasks;

};

 

void ThreadPool::Init()

{

         m_pTasks = new ThreadTaskPool;

         for(int nIdx = 0; nIdx < MAX_THREADS; ++nIdx)

         {

                  m_hThreads[nIdx] = CreateThread(NULL, 0, ThreadProc, this, 0, NULL);

}

}

 

上述代码实现了一个简单的线程池,Task接口用于定义任务,一切需要交给线程池处理的任务都派生自该接口。而线程池中的所有线程只在其线程处理函数中做一件事:检查任务管理器中是否能取出新的任务来,如果能就干活,不能就调用Sleep交出时间片。(这里有一个可以优化的地方,我们可以在任务管理器中定义一个事件对象,如果线程取不出任务,则等待该事件对象,如果有新任务被PushTask,则该事件对象被激活,并唤醒一个等待中的线程)

 

上面的两个例子:编辑器中的撤销/重做,以及线程池处理任务队列,都是实际编程当中对Command模式的典型应用。他们的共同点在于:都有一个统一的动作接口,而接口背后的工作可能多种多样。在实际编程当中遇到具有这种特点的问题,便可以采用这种思想进行抽象,有助于我们编写具有良好扩展性的程序。