Posts tagged Command
小议Command模式 抽象在实际编程中的应用
0在许多编辑器中,大都实现了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模式的典型应用。他们的共同点在于:都有一个统一的动作接口,而接口背后的工作可能多种多样。在实际编程当中遇到具有这种特点的问题,便可以采用这种思想进行抽象,有助于我们编写具有良好扩展性的程序。