Mediator/Python
我现在就坦白承认,我是一个 Linux 新手,大约两年前换工作后才进入 UNIX 环境。在此之前,我是一名嵌入式系统程序员,主要在 Windows 环境下工作。如果您能原谅我,我想说 UNIX/Linux 的理念一直在逐渐征服我。除了学习这个令人惊叹的操作系统之外,我还有机会接触到 Python 语言。我不仅发现 Python 是一种强大而富有表现力的工具,而且随着 wxPython 作为 GUI 扩展的加入,我不会再回到 Win32 API 编程了。
我在 GUI 工作中经常做的事情之一是开发对话框。我的部分设计目标是使对话框上的控件具有交互性,以便控件的行为能够帮助我的用户完成他们的工作。您可能见过一些应用程序这样做,其中一个或多个控件的状态会根据某些其他控件的状态而改变。这种交互可以通过在这些控件的事件处理程序中放置代码来创建。但是,您最终会在这些事件处理程序中编写大量代码才能很好地完成所有这些工作。此外,将交互编码到事件处理程序中会将控件耦合在一起。这意味着它们必须彼此了解才能进行交互。如果您的对话框变得复杂,可能会导致维护噩梦。想象一下所有这些控件都在决定其当前状态对对话框上的所有其他控件意味着什么。简直就是意大利面条式代码!
当我创建这种对话框时,我可以看到它会导致混乱,所以我希望将控件彼此解耦。我还希望将交互代码集中在一个地方,这样更改只需要在一个地方更新。诀窍是决定如何做到这一点。我尝试过一件事,我相信很多人也这样做,那就是在网上搜索看看是否有人做过类似的事情。我发现的是关于设计模式的讨论,特别是《设计模式:可复用面向对象软件的基础》这本书,我强烈推荐给任何认真对待代码的人。书中的例子是基于 C++ 的,但这些概念适用于许多基于 OO 的语言,包括 Python。我们将要使用的解决对话框问题的模式称为 Mediator 模式。这种模式封装了一组对象的交互。
从 OO 的角度来看,有一个 Mediator 对象,其中包含我们希望相互交互的所有对象;它们被称为同事 (Colleagues)。同事对 Mediator 对象具有弱引用,彼此之间没有引用。Mediator 对同事对象具有强引用,并且可以直接更新它们,以便为所有同事对象创建所需的行为。这种模式的好处正是我们所追求的;它集中了对象的交互并减少了它们之间的耦合。
Mediator/Colleague 模式被实现为一个可以构建实际类对象的接口。Mediator 接口有一个名为 ColleagueChanged() 的方法,所有同事都会调用该方法来通知 Mediator 发生了更改。Colleague 接口只有一个必需的方法,称为 Changed(),每个派生对象都会调用该方法来通知 Mediator 状态发生了更改。此外,Colleague 基类还有一个公共数据成员,名为 mediator,它是对其包含的 Mediator 对象的引用。
所有这些都非常好,但是我们实际上如何实现一个使用 Mediator/Colleague 模式的对话框呢?我们将使用 Python 的 OO 特性和 wxPython 作为窗口的 GUI 界面来实现这一点。不过,首先要做的是;让我们创建一个 Mediator 基类
class Mediator: def __init__(self): pass def ColleagueChanged(self, control, event): self._ColleagueChanged(control, event) def _ColleagueChanged(self, control, event): pass
在这个例子中,Mediator 类被实现为模板模式。这种模式允许我们分离类的接口和实现。这个类的用户调用 CreateColleagues() 方法,但在他们的派生类中重写 _CreateColleagues() 方法。我不会进一步讨论模板模式,但它是另一个非常有用的模式。
现在让我们创建我们的 Colleague 基类
class Colleague: def __init__(self, mediator): self.mediator = mediator def Changed(self, colleague, event): self._Changed(colleague, event) def _Changed(self, colleague, event): self.mediator.ColleagueChanged(colleague, event)
Colleague 基类也被实现为模板模式。和之前一样,用户调用 Changed() 方法,但必要时重写 _Changed() 方法。此外,Colleague 还有一个数据成员 self.mediator,它是对包含它的 Mediator 实例的引用。这个引用被传递到 Colleague 的构造函数中。
由于我们的示例程序旨在展示 Mediator/Colleague 模式的实用性,因此它有些人为设计。为了使示例更简单一些,我将示例的单个窗口基于 wxPython 库中的 wxFrame,而不是 wxDialog。否则,代码是相同的。因为我们的示例程序使用 wxFrame 作为控件的容器,所以它是作为 Mediator 的逻辑选择。为了给 wxFrame 提供 Mediator 类的接口,我们将创建一个新的类 MainFrame,如下所示
class MainFrame(wxFrame, Mediator): def __init__(self, parent, ID, title): wxFrame.__init__(self, parent, ID, title, wxDefaultPosition, wxSize(400, 300)) Mediator.__init__(self)
在这段代码中,我们创建了一个新的类 MainFrame,它继承自 wxFrame 和 Mediator 基类。为了使其在 Python 中工作,我们必须显式调用两个父类的构造函数。这个调用是在 MainFrame 类的 __init__() 方法中进行的。
为了使我们的 MainFrame 类与其控件作为 Colleagues 交互,这些控件必须派生自 Colleague 基类。作为一个例子,让我们创建一个文本控件,它也是一个 Colleague 对象。我们通过创建一个新的类 myTextCtrl 来实现这一点,如下所示
class myTextCtrl(wxTextCtrl, Colleague): def __init__(self, mediator, *_args, **_kwargs): apply(wxTextCtrl.__init__, (self,) + _args, _kwargs) Colleague.__init__(self, mediator)
在这里,我们创建了一个新的类,它继承自 wxTextCtrl 和 Colleague 基类。同样,为了使其在 Python 中工作,我们必须显式调用两个父类的构造函数。这在 myTextCtrl 的 __init__() 方法中完成。为了使 wxTextCtrl 类的所有可选参数都正确地传递给其构造函数,我将使用 apply() 函数来调用其 __init__() 方法并传入参数。Colleague 基类的 __init__() 方法被直接调用,传递 mediator 作为参数。
现在我们有一个类,它既是 Colleague 又是 wxTextCtrl。这个类的实例将接收 wxTextCtrl 生成的所有事件,并且也具有 Colleague 类的行为。当发生 MainFrame 对象关心的事件时,事件处理程序调用 Changed() 方法,并将 self 引用和事件作为参数传递。正如 Colleague 类中定义的那样,Changed() 方法调用控件的 Mediator 引用的 ColleagueChanged() 方法。通过这种方式,MainFrame 对象(它是 Mediator 对象)被告知其包含的控件中发生的所有更改。
那么我们如何在 MainFrame 窗口中将所有这些联系在一起呢?首先,就像我们对 myTextCtrl 所做的那样,我们必须创建所有将在窗口上交互的控件的派生类,这些派生类也派生自 Colleague 基类。然后,与大多数 wxPython 窗口一样,我们必须在窗口的构造函数中创建我们的控件;在本例中是 MainFrame 的 __init__() 方法。每次创建控件时,MainFrame 都会将自身作为参数传递给派生控件。您将在本文随附的完整示例程序中看到这一点 [可在 ftp.linuxjournal.com/pub/lj/listings/issue98/5858.tgz 获取]。不要对 MainFrame.__init__() 方法中的大量代码感到沮丧;其中很大一部分调用了 wxPython 提供的布局功能,对于我们的示例来说并非绝对必要。它只是让它看起来更漂亮。
您应该注意到我在 MainFrame.__init__() 方法中做的一件事是创建了一个名为 self.__colleagueMap 的字典对象。我已经将键/值对集合放入这个字典中,包括对创建的 Colleague 控件的引用和 MainFrame 类的方法。我这样做是因为 Python 没有像 C/C++ 那样的 switch/case 结构。这个字典提供了一种优雅的机制,可以在 Colleague 对象发生更改时调用正确的方法,而无需使用冗长的 if/elseif 结构。您将在示例程序中看到这一点,在 _ColleagueChanged() 方法的实现中,如下所示
def _ColleagueChanged(self, colleague, event): if self.__inProcess != true: self.__inProcess = true if self.__colleagueMap.has_key(colleague): self.__colleagueMap[colleague](event) self.__inProcess = false
在这段代码中,参数 colleague 被用作字典的查找键。如果同事作为键存在,则调用相应的方法并将事件作为参数传递。这是一种非常巧妙的方式来实现像 switch/case 结构这样的多路分支。
现在我们的 Mediator 和 Colleague 对象存在并且连接在一起。Mediator 对象 (MainFrame) 将被告知 Colleague 对象(控件)生成的任何相关事件。那么还剩下什么要做呢?我们必须提供集中的代码,这些代码将创建控件之间所需的交互。这项工作是在我们放入 self.__colleagueMap 字典的方法中完成的。在每个方法中,我们都放置了对该控件事件做出反应的代码。由于这些方法是我们 Mediator (MainFrame) 对象的一部分,因此它们了解并可以访问窗口上的所有其他控件。
示例程序已经在 Python 版本 2.1 和 2.2 以及它们各自的 wxPython 版本下进行了测试。当示例程序运行时,您应该看到一个打开的窗口,看起来如图 1 所示。
显示窗口上的交互涉及大多数控件。当您在文本框中键入字符时,程序会进行一些简单的速度选择,并突出显示列表中第一个匹配的条目。此外,“选择”和“清除”按钮变为启用状态。如果您选择城市或州,则会在另一个列表框中进行互补选择。如果您使用单选按钮选择不同的区域,则该操作会重新初始化列表框,清除文本框并禁用“选择”和“清除”按钮。单独单击“清除”按钮会清除文本框并禁用自身。所有交互代码都在 MainFrame 类中,并且控件彼此不耦合。
我们的 wxFrame 窗口并没有做任何非常有用的事情,但它确实演示了 Mediator 模式如何编排窗口上控件的行为。当您将此模式应用于更复杂的对话框时,这种努力的真正回报就会到来,在更复杂的对话框中,交互复杂性可能会以惊人的速度增长。为了管理它,我们所要做的就是添加另一个派生自 Colleague 类的控件,并将相应的交互代码添加到我们的 ColleagueChanged() 方法中,这样复杂性就得到了处理。

Doug Farrell 是 Scholastic, Inc. 位于康涅狄格州办公室的高级软件工程师。他开发 Web 应用程序以将参考书目放在 Internet 上。当他不挠头解决一些编程难题时,您可以发现他和他的妻子 Susan 骑着自行车在路上飞驰。