When a program is made up of a number of classes, the logic and computation is divided logically among these classes. However, as more of these isolated classes are developed in a program, the problem of communication between these classes becomes more complex. The more each class needs to know about the methods of another class, the more tangled the class structure can become. This makes the program harder to read and harder to maintain. Further, it can become difficult to change the program, since any change may affect code in several other classes. The Mediator pattern addresses this problem by promoting looser coupling between these classes. Mediators accomplish this by being the only class that has detailed knowledge of the methods of other classes. Classes inform the Mediator when changes occur, and the Mediator passes on the changes to any other classes that need to be informed.
Let's consider a program that has several buttons, two list boxes, and a text entry field, as shown in Figure 26-1.
When the program starts, the Copy and Clear buttons are disabled.
When you select one of the names in the left-hand list box, it is copied into the text field for editing, and the Copy button is enabled.
When you click on Copy, that text is added to the right-hand list box, and the Clear button is enabled, as we see in Figure 26-2.
If you click on the Clear button, the right-hand list box and the text field are cleared, the list box is deselected, and the two buttons are again disabled.
User interfaces such as this one are commonly used to select lists of people or products from longer lists. Further, they are usually even more complicated than this one, involving insert, delete, and undo operations as well.
The interactions between the visual controls are pretty complex, even in this simple example. Each visual object needs to know about two or more others, leading to quite a tangled relationship diagram, as shown in Figure 26-3.
Figure 26-3. A tangled web of interactions between classes in the simple visual interface we presented in Figures 26-1 and 26-2
The Mediator pattern simplifies this system by being the only class that is aware of the other classes in the system. Each of the controls with which the Mediator communicates is called a Colleague. Each Colleague informs the Mediator when it has received a user event, and the Mediator decides which other classes should be informed of this event. This simpler interaction scheme is illustrated in Figure 26-4.
The advantage of the Mediator is clear: It is the only class that knows of the other classes and thus the only one that would need to be changed if one of the other classes changes or if other interface control classes are added.
Let's consider this program in detail and decide how each control is constructed. The main difference in writing a program using a Mediator class is that each class needs to be aware of the existence of the Mediator. You start by creating an instance of the Mediator and then pass the instance of the Mediator to each class in its init method.
Set med = New Mediator med.registerKidList lsKids med.registerPicked lsPicked med.registerText txName med.init
Our two buttons use accompanying Command pattern classes and register themselves with the Mediator during their initialization. Here is the CopyCommand class.
'Class CopyCommand Implements Command Private med As Mediator Public Sub init(md As Mediator, cpBut As CommandButton) Set med = md med.registerCopy cpBut End Sub Private Sub Command_Execute() med.copyClicked End Sub
The Clear button is exactly analogous.
The Kid name list is based on the one we used in the last two examples but expanded so that the data loading of the list takes place in the Mediator's init method.
Public Sub init() 'init method for Mediator Dim kds As New Kids 'Kids class instabce Dim kd As Kid Dim iter As Iterator kds.init App.Path & "50free.txt" 'read in file Set iter = kds.getIterator 'get iterator While iter.hasMoreElements 'put names in list box Set kd = iter.nextElement kidList.AddItem kd.getFrname & " " & kd.getLname Wend clearClicked End Sub
The text field is even simpler, since all we have to do is register it with the Mediator. The complete Form_Load event for the list box is shown here with all the registration and command classes.
Private Sub Form_Load() Set med = New Mediator 'create mediator Set cpyCmd = New CopyCommand 'copy command class cpyCmd.init med, btCopy Set clrCmd = New ClearCommand 'clear command class clrCmd.init med, btClear med.registerKidList lsKids 'register lists med.registerPicked lsPicked 'and text box med.registerText txName med.init 'set all to beginning state End Sub
The general point of all these classes is that each knows about the Mediator and tells the Mediator of its existence so the Mediator can send commands to it when appropriate.
The Mediator itself is very simple. It supports the Copy, Clear, and Select methods and has register methods for each of the controls.
Option Explicit 'Class Mediator Private copyButton As CommandButton Private clearButton As CommandButton Private txtBox As TextBox Private kidList As ListBox Private pickedList As ListBox '----- Public Sub registerCopy(cpBut As CommandButton) Set copyButton = cpBut 'copy button End Sub '----- Public Sub copyClicked() pickedList.AddItem txtBox.Text 'add text to picked list clearButton.Enabled = True 'enable clear button kidList.ListIndex = -1 'deselect list item End Sub '----- Public Sub registerClear(clrBut As CommandButton) Set clearButton = clrBut 'clear button End Sub '----- Public Sub clearClicked() txtBox.Text = "" 'clear text bos copyButton.Enabled = False 'disable buttons clearButton.Enabled = False pickedList.Clear 'clear picked list kidList.ListIndex = -1 'deselect list item End Sub '----- Public Sub registerText(txt As TextBox) Set txtBox = txt 'text box End Sub '----- Public Sub registerKidList(klist As ListBox) Set kidList = klist 'kid list End Sub '----- Public Sub registerPicked(plist As ListBox) Set pickedList = plist 'picked list End Sub '----- Public Sub listClicked() Dim i As Integer i = kidList.ListIndex If (i >= 0) Then txtBox.Text = kidList.Text End If copyButton.Enabled = True End Sub '----- Public Sub init() 'init method for Mediator Dim kds As New Kids 'Kids class instance Dim kd As Kid Dim iter As Iterator kds.init App.Path & "50free.txt" 'read in file Set iter = kds.getIterator 'get iterator While iter.hasMoreElements 'put names in list box Set kd = iter.nextElement kidList.AddItem kd.getFrname & " " & kd.getLname Wend clearClicked 'Set to initial state End Sub
One further operation that is best delegated to the Mediator is the initialization of all the controls to the desired state. When we launch the program, each control must be in a known, default state, and since these states may change as the program evolves, we simply create an init method in the Mediator, which sets them all to the desired state. In this case, that state is the same as the one achieved by the Clear button, and we simply call that method this.
clearClicked 'Set to initial state
The two buttons in this program use command objects. Just as we noted earlier, this makes processing of the button click events quite simple.
Private Sub btClear_Click() med.clearClicked End Sub '----- Private Sub btCopy_Click() med.copyClicked End Sub '----- Private Sub lsKids_Click() med.listClicked End Sub
In either case, however, this represents the solution to one of the problems we noted in the Command pattern chapter: Each button needed knowledge of many of the other user interface classes in order to execute its command. Here, we delegate that knowledge to the Mediator, so the Command buttons do not need any knowledge of the methods of the other visual objects. The class diagram for this program is shown in Figure 26-5, illustrating both the Mediator pattern and the use of the Command pattern.
You can create a Mediator in much the same way in VB7, but you can take advantage of inheritance to make your work easier. The Copy and Clear buttons and the Kid name list can all be subclassed from the standard controls so that they support the Command interface and register themselves with the Mediator during the constructor. This makes the derived button classes very easy to write.
Public Class CpyButton Inherits System.Windows.Forms.Button Implements Command Private med As Mediator Public Sub setMediator(ByVal md As Mediator) med = md med.register(Me) End Sub '---- 'tell the Mediator we've been clicked Public Sub Execute() Implements Command.Execute med.copyClicked() End Sub End Class
Further, since VB7 supports polymorphism, we can have a register method in the Mediator with different argument types for each control we want to register. These methods are shown here.
Public Overloads Sub register(ByVal cpb As CopyButton) cpbutton = cpb End Sub '----- Public Overloads Sub register(ByVal clr _ As ClearButton) clrbutton = clr End Sub '----- Public Overloads Sub register(ByVal kd _ As KidsListBox) klist = kd End Sub '----- Public Overloads Sub register(ByVal pick As ListBox) pklist = pick End Sub '----- Public Overloads Sub register(ByVal tx As TextBox) txkids = tx End Sub
The remainder of the Mediator manipulates the various controls as before.
Public Sub kidPicked() 'copy text from list to textbox txkids.Text = klist.Text 'copy button enabled cpbutton.Enabled = True End Sub '----- Public Sub copyClicked() 'copy name to picked list pklist.Items.Add(txkids.Text) 'clear button enabled clrbutton.Enabled = True klist.SelectedIndex = -1 End Sub '----- Public Sub clearClicked() 'disable buttons and clear list cpbutton.Enabled = False clrbutton.Enabled = False pklist.Items.Clear() End Sub '-----
When we create the controls, we start by creating an instance of the Mediator. Then as the buttons and list box controls are created, they can register themselves inside the constructor for each derived control.
Private Sub init() 'called from New constructor Dim evh As EventHandler evh = New EventHandler(AddressOf CommandHandler) med = New Mediator() btCopy.setMediator(med) btClear.setMediator(med) kList.setMediator(med) 'register remaining controls med.register(txName) med.register(lsPicked) med.init() 'initialize mediator
During initialization, the Mediator reads in the data file and puts the kid's names in the kidList list box. Note that the Kids class does the reading as before, using the vbFile class, and that the Mediator just provides the filename and loads the list once the file is read.
Public Sub init() 'initializes the Mediator's objects Dim kd As Kid clearClicked() 'set to defaults 'read in datafile and load list kds = New Kids(Application.StartupPath & "50free.txt") Dim iter As Iterator = kds.getIterator 'Note we use the iterator here While (iter.hasMoreElements) kd = CType(iter.nextElement, Kid) klist.Items.Add(kd.getFrname + " " + kd.getLname) End While End Sub
We create the new classes CopyButton, ClearButton, and KidListBox, and rather than declaring them as WithEvents, we simply add an event handler to each of them, which is the same simple handler in all three cases, and include this handler registration in the Forms init method called from its constructor.
AddHandler btCopy.Click, evh AddHandler btClear.Click, evh AddHandler kList.SelectedIndexChanged, evh
Now, the two buttons clicks and selecting a kid in the Listbox all call the CommandHandler. Since all three classes implement the Command interface, our command handler reduces to just two lines of code.
Public Sub CommandHandler(ByVal sender As Object, _ ByVal e As System.EventArgs) Dim cmd As Command = CType(sender, Command) cmd.Execute() End Sub
You can appreciate the simplification VB7 makes in using a Mediator by examining the UML diagram shown in Figure 26-6.
The Mediator pattern keeps classes from becoming entangled when actions in one class need to be reflected in the state of another class.
Using a Mediator makes it easy to change a program's behavior. For many kinds of changes, you can merely change or subclass the Mediator, leaving the rest of the program unchanged.
You can add new controls or other classes without changing anything except the Mediator.
The Mediator solves the problem of each Command object needing to know too much about the objects and methods in the rest of a user interface.
The Mediator can become a “god class,” having too much knowledge of the rest of the program. This can make it hard to change and maintain. Sometimes you can improve this situation by putting more of the function into the individual classes and less into the Mediator. Each object should carry out its own tasks, and the Mediator should only manage the inter action between objects.
Each Mediator is a custom-written class that has methods for each Colleague to call and knows what methods each Colleague has available. This makes it difficult to reuse Mediator code in different projects. On the other hand, most Mediators are quite simple, and writing this code is far easier than managing the complex object interactions any other way.
The Mediator pattern described here acts as a kind of Observer pattern, observing changes in each of the Colleague elements, with each element having a custom interface to the Mediator. Another approach is to have a single interface to your Mediator and pass to that method various objects that tell the Mediator which operations to perform.
In this approach, we avoid registering the active components and create a single action method with different polymorphic arguments for each of the action elements.
Public Sub action(mv As MoveButton) Public Sub action(clrButton As ClearButton) Public Sub action(klst as KidList)
Thus, we need not register the action objects, such as the buttons and source list boxes, since we can pass them as part of generic action methods.
In the same fashion, you can have a single Colleague interface that each Colleague implements, and each Colleague then decides what operation it is to carry out.
Mediators are not limited to use in visual interface programs; however, it is their most common application. You can use them whenever you are faced with the problem of complex intercommunication between a number of objects.