10.5 命令:选择操作
命令(command)模式的结构很简单,但是对于消除代码间的耦合(decoupling)—清理代码—却有着重要的影响。
在《Advanced C++:Programming Styles And Idioms》(Addison Wesley,1992)一书中,Jim Coplien创造了术语函子(functor),它表示一个对象,该对象的惟一目的是封装一个函数(由于“函子”在数学上有其特定的意义,这里将用更加明确的术语函数对象(function object)来代替它)。其特点就是消除被调用函数的选择与那个函数被调用的位置之间的联系。
GoF书中也提到这个术语,但是没有使用。然而,函数对象的话题却在那本书的很多模式中被反复论及。
从最直观的角度来看,命令模式就是一个函数对象:一个作为对象的函数。通过将函数封装为对象,就能够以参数的形式将其传递给其他函数或者对象,告诉它们在履行请求的过程中执行特定的操作。可以说,命令模式是携带行为信息的信使。
命令模式的主要特点是允许向一个函数或者对象传递一个想要的动作。上述例子提供了将一系列需要一起执行的动作集进行排队的方法。在这里,可以动态创建新的行为,某些事情通常只能通过编写新的代码来完成,而在上述例子中可以通过解释一个脚本来实现(如果需要实现的东西很复杂请参考解释器模式)。
GoF认为“命令模式是回调(callback)的面向对象的替代物”,然而这里的单词“back”是回调概念的重要的一部分—回调返回到回调的创建者所在的位置。另一方面,对于一个命令对象来说,典型的做法仅仅是创建它并且将之传递给一些函数或者对象,而不是自始至终以其他方式联系命令对象。
命令模式的一个常见的例子就是在应用程序中“撤销(undo)操作”功能的实现。每次在用户进行某项操作的时候,相应的“撤销操作”命令对象就被置入一个队列中。而每个命令对象被执行后,程序的状态就倒退一步。
利用命令模式消除与事件处理的耦合
正如读者将在下一章中要看到的,采用并发(concurrency)技术的原因之一是为了更容易地掌握事件驱动编程(event-driven programming),在事件驱动方式的编程中,这些事件出现的地方是不可预料的。例如,当程序正在执行一个操作时,用户按下“退出”按钮并且希望程序能够快速响应。
使用并发的论据是它能够防止程序中代码段间的耦合。也就是说,如果运行一个独立的线程用于监视退出按钮,程序的“正常”操作无须知道有关退出按钮或者其他需要监视的操作。
然而,一旦读者理解耦合是一个问题,就可以用命令模式来避免它。每个“正常”的操作必须周期性地调用一个函数来检查事件的状态,而通过命令模式,这些“正常”操作不需要知道有关它们所检查的事件的任何信息,也就是说它们已经与事件处理代码分离开来。
在这里,命令对象由被单件TaskRunner执行的Task表示。EventSimulator创建一个随机延迟时间,所以当周期性的调用函数fired()时,在某个随机时间段,其返回结果从true到false变化。EventSimulator对象在类Button中使用,模拟在某个不可预知的时间段用户事件发生的动作。CheckButton是Task的实现,在程序中通过所有“正常”代码对其进行周期性的检查—可以看到这些检查发生在函数Procedure1()、Procedure2()和Procedure3()的末尾。
尽管这需要颇费点脑筋来设立命令对象,但是读者将在第11章中看到,如果采用线程处理方法则需要更多的考虑,小心预防并行编程中与生俱来的各种的困难问题,所以这种较简便的解决方法更可取。将TaskRunner:run()调用植入一个多线程处理的“计时器”对象中,也可以创建一个很简单的线程处理方案。这样做,可以消除所有“正常操作”(上述例子中的过程)与事件代码间的耦合。