Using Python and the Mediator pattern to make dialog boxes that are smart and responsive without creating a coding mess.
I'll confess right now and tell you that I'm a newbie to Linux, having moved into a UNIX environment when I changed jobs almost two years ago. Before that I was an embedded systems programmer who worked primarily in a Windows environment. If you can find it in your heart to forgive me, I'd like to say the UNIX/Linux philosophy has been steadily winning me over. In addition to learning this amazing OS, I've had the opportunity to be exposed to the Python language. Not only do I find Python to be a powerful and expressive tool, but with the addition of wxPython as a GUI extension, I won't be going back to programming the Win32 API.
One of the things I did regularly in my GUI work was develop dialog boxes. Part of my design goal was to make the controls on the dialog boxes interactive, so the behavior of the controls would help my users get their work done. You've probably seen applications that do this, where one or more controls will change state depending on the state of some other control. This kind of interaction can be created by placing code in the event handlers of these controls. However, you'll end up writing a great deal of code in these event handlers to do all of this well. Plus, coding the interaction into the event handler couples the controls together. This means they have to know about each other in order to interact. If your dialog box gets at all complicated, it can lead to a maintenance nightmare. Just picture all of those controls making decisions about what their current state means to all the other controls on the dialog box. Talk about spaghetti code!
When I was creating this kind of dialog box I could see it would lead to a mess, so I wanted to decouple the controls from one another. I also wanted to centralize the interaction code in one place, so changes would have to be updated in only one place. The trick was to decide how to do this. One thing I tried, and I'm sure many others do this as well, was to search the Web to see if anyone had done something similar. What I found were discussions of design patterns and, specifically, the book Design Patterns: Elements of Reusable Object-Oriented Software, which I highly recommend to anyone who takes their code seriously. The examples in the book are based on C++, but the concepts apply to many OO-based languages, including Python. The pattern we're going to use as a solution to our dialog box problem is called the Mediator pattern. This pattern encapsulates the interactions of a set of objects.
From an OO point of view, there is one Mediator object containing all the objects that we want to have interact with one another; they are called Colleagues. The Colleagues have a weak reference to the Mediator object and none to each other. The Mediator has strong reference to the Colleague objects and can update them directly in order to create the desired behavior for all Colleague objects. The benefits of this pattern are what we're looking for; it centralizes the interaction of objects and reduces the coupling between them.
The Mediator/Colleague pattern is implemented as an interface that can build actual class objects. The Mediator interface has a method called ColleagueChanged(), which is what all Colleagues call to inform the Mediator that a change has occurred. The Colleague interface has only one required method, called Changed(), which each derived object calls to inform the Mediator that a change in state has occurred. In addition, the Colleague base class has a public data member called mediator, which is a reference to the Mediator object that contains it.
All of that's very nice, but how do we actually implement a dialog box that uses the Mediator/Colleague pattern? We'll do this using Python's OO features and using wxPython as the GUI interface for a window. First things first, though; let's create a Mediator base class:
class Mediator: def __init__(self): pass def ColleagueChanged(self, control, event): self._ColleagueChanged(control, event) def _ColleagueChanged(self, control, event): pass
In this example the Mediator class is implemented as a Template pattern. This pattern allows us to separate the interface and implementation of the class. Users of this class call the CreateColleagues() method but override the _CreateColleagues() method in their derived classes. I won't discuss the Template pattern further, but it's another very useful pattern to know.
Now let's create our Colleague base class:
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)
The Colleague base class is also implemented as a Template pattern. As before, users call the Changed() method but override the _Changed() method, if necessary. In addition, the Colleague has a data member, self.mediator, which is a reference to the containing Mediator instance. This reference is passed into the constructor of the Colleague.
Since our example program is intended to show the utility of the Mediator/Colleague pattern, it's somewhat contrived. To make the example a little simpler I've based the one window of the example on wxFrame from the wxPython library, rather than wxDialog. Otherwise, the code is the same. Because our example program uses wxFrame as the container of the controls, it's the logical choice to be the Mediator. In order to give wxFrame the interface of the Mediator class, we'll create a new class MainFrame that looks like this:
class MainFrame(wxFrame, Mediator): def __init__(self, parent, ID, title): wxFrame.__init__(self, parent, ID, title, wxDefaultPosition, wxSize(400, 300)) Mediator.__init__(self)
In this code we've created a new class, MainFrame, that inherits from both wxFrame and Mediator base classes. For this to work in Python we have to call explicitly the constructor of both the parent classes. This call is made in the __init__() method of the MainFrame class.
In order for our MainFrame class to interact with its controls as Colleagues, those controls have to be derived from the Colleague base class. As an example, let's create a text control that is also a Colleague object. We do this by creating a new class, myTextCtrl, that looks like this:
class myTextCtrl(wxTextCtrl, Colleague): def __init__(self, mediator, *_args, **_kwargs): apply(wxTextCtrl.__init__, (self,) + _args, _kwargs) Colleague.__init__(self, mediator)
Here we've created a new class that inherits from both wxTextCtrl and Colleague base classes. Again, in order for this to work in Python we have to call explicitly the constructor of both the parent classes. This is done in the __init__() method of myTextCtrl. In order to get all the optional parameters of the wxTextCtrl class properly passed to its constructor, I'll use the apply() function to call its __init__() method and pass in the parameters. The __init__() method of the Colleague base class is called directly, passing mediator as a parameter.
Now we have a class that is both a Colleague and a wxTextCtrl. An instance of this class will receive all the events generated by a wxTextCtrl and also has the behavior of the Colleague class. When an event occurs that our MainFrame object cares about, the event handler calls the Changed() method and passes the self reference and the event as parameters. As defined in the Colleague class, the Changed() method calls the ColleagueChanged() method of the control's Mediator reference. In this way the MainFrame object (which is a Mediator object) is informed of all changes occurring in its contained controls.
So how do we tie all this together in our MainFrame window? First, as we did with myTextCtrl, we must create derived classes of all the controls that will interact on our window that also derive from the Colleague base class. Then, as in most wxPython windows, we must create our controls in the window's constructor; in this case MainFrame's __init__() method. Each time a control is created, MainFrame passes itself as a parameter to the derived control. You'll see this in the complete example program that accompanies this article [available at ftp.ssc.com/pub/lj/listings/issue98/5858.tgz]. Don't be dismayed by the amount of code in the MainFrame.__init__() method; a great deal of it calls layout functionality provided by wxPython and is not strictly necessary for our example. It just makes it look nicer.
One thing you should notice I've done in the MainFrame.__init__() method is create a dictionary object called self.__colleagueMap. I've placed sets of key/value pairs into this dictionary consisting of a reference to the created Colleague controls and a method of the MainFrame class. I've done this because Python does not have a switch/case construct like C/C++ has. This dictionary provides an elegant mechanism to call the correct method whenever a Colleague object has changed, without resorting to a lengthy if/elseif construct. You'll see this in the example program in the implementation of the _ColleagueChanged() method, as shown below:
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
In this code the parameter, colleague, is used as the lookup into the dictionary. If the colleague exists as a key, then the corresponding method is called and the event is passed as a parameter. This is a very slick way to implement a multiway branch like the switch/case construct.
Now our Mediator and Colleague objects exist and are connected together. The Mediator object (MainFrame) will be notified of any relevant event generated by a Colleague object (control). So what's left to do? We have to provide the centralized code that will create the desired interaction between our controls. This work is done in the methods we've placed into the self.__colleagueMap dictionary. Into each method we place the code that reacts to that controls event. Since these methods are part of our Mediator (MainFrame) object, they know about and have access to all other controls on the window.
The example program has been tested under Python versions 2.1 and 2.2, along with their respective wxPython versions. When the example program is run you should see a window open up that looks like the one pictured in Figure 1.
The interaction on the displayed window involves most of the controls. When you type a character in the text box, the program does some simple speed selection and highlights the first entry in the list box that matches. In addition, the Select and Clear buttons become enabled. If you select either a city or state, the complementary selection is made in the other list box. If you select a different region with the radio buttons, that action re-initializes the lists boxes, clears the text box and disables the Select and Clear buttons. Clicking on the Clear button alone clears the text box and disables itself. All of the interaction code is in the MainFrame class, and the controls are not coupled to one another.
Our wxFrame window doesn't do anything very useful, but it does demonstrate how the Mediator pattern can orchestrate the behavior of controls on a window. The real payoff for this effort comes when you apply this pattern to a more involved dialog box, where the interaction complexity can grow at a shocking rate. To manage it, all we have to do is add another control that is derived from the Colleague class and add the corresponding interaction code to our ColleagueChanged() method, and the complexity is handled.