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 two buttons, two list boxes, and a text entry field, as shown in Figure 25-1.
Figure 25-1. A simple program with two lists, two buttons, and a text field that will interact
When the program starts, the Copy and Clear buttons are disabled.
Figure 25-2. When you select a name, the buttons are enabled, and when you click on Copy, the name is copied to the right list box.
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 25-3.
Figure 25-3. A tangled web of interactions between classes in the simple visual interface we presented in Figures 25-1 and 25-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 25-4.
Figure 25-4. A Mediator class simplifies the interactions between classes.
The advantage of the Mediator is clear: It is the only class that is aware 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 your Mediator class and then pass the instance of the Mediator to each class in its constructor.
med = new Mediator (btCopy, btClear, lsKids, lsSelected);
btCopy.setMediator (med); //set mediator ref in each control
btClear.setMediator (med);
lsKids.setMediator (med);
med.setText (txName); //tell mediator about text box
We derive our two button classes from the Button class, so they can also implement the Command interface. These buttons are passed to the Mediator in its constructor. Here is the CpyButton class.
public class CpyButton : System.Windows.Forms.Button, Command {
private Container components = null;
private Mediator med;
//-----
public CpyButton() {
InitializeComponent();
}
//-----
public void setMediator(Mediator md) {
med = md;
}
//-----
public void Execute() {
med.copyClicked ();
}
Its Execute method simply tells the Mediator class that it has been clicked, and it lets the Mediator decide what to do when this happens. The Clear button is exactly analogous.
We derive the KidList class from the ListBox class and have it loaded with names within the Mediator’s constructor.
public Mediator(CpyButton cp, ClrButton clr, KidList kl,
ListBox pk) {
cpButton = cp; //copy in buttons
clrButton = clr;
klist = kl; //copy in list boxes
pkList = pk;
kds = new KidData ("50free.txt"); //create data list class
clearClicked(); //clear all controls
KidIterator kiter = kds.getIterator ();
while(kiter.MoveNext () ) { //load list box
Kid kd = (Kid) kiter.Current ;
klist.Items .Add (kd.getFrname() +" "+
kd.getLname ());
}
}
We don’t have to do anything special to the text field, since all its activity takes place within the Mediator. We just pass it to the Mediator using the setText method as we just illustrated.
The only other important part of our initialization is creating a single event handler for the two buttons and the list box. Rather than letting the development environment generate these click events for us, we create a single event and add it to the click handlers for the two buttons and the list box’s SelectIndexChanged event. The intriguing thing about this event handler is that all it needs to do is call each control’s Execute method and let the Mediator methods called by those Execute methods do all the real work.
The event handler for these click events is simply as follows.
//each control is a command object
public void clickHandler(object obj, EventArgs e) {
Command comd = (Command)obj; //get command object
comd.Execute (); //and execute command
}
We show the complete Form initialization method that creates this event connections below.
private void init() {
//set up mediator and pass in referencs to controls
med = new Mediator (btCopy, btClear, lsKids, lsSelected);
btCopy.setMediator (med); // mediator ref in each control
btClear.setMediator (med);
lsKids.setMediator (med);
med.setText (txName); //tell mediator about text box
//create event handler for all command objects
EventHandler evh = new EventHandler (clickHandler);
btClear.Click += evh;
btCopy.Click += evh;
lsKids.SelectedIndexChanged += evh;
}
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 a register method for the TextBox. The two buttons and the ListBox are passed in the Mediator’s constructor. Note that there is no real reason to choose setXxx methods over constructor arguments for passing in references to these controls. We simply illustrate both approaches in this example.
public class Mediator {
private CpyButton cpButton; //buttons
private ClrButton clrButton;
private TextBox txKids; //text box
private ListBox pkList; //list boxes
private KidList klist;
private KidData kds; //list of data from file
public Mediator(CpyButton cp, ClrButton clr,
KidList kl, ListBox pk) {
cpButton = cp; //copy in buttons
clrButton = clr;
klist = kl; //copy in list boxes
pkList = pk;
kds = new KidData ("50free.txt"); //create data list
clearClicked(); //clear all controls
KidIterator kiter = kds.getIterator ();
while(kiter.MoveNext () ) { //load list box
Kid kd = (Kid) kiter.Current ;
klist.Items .Add (kd.getFrname() +
" "+kd.getLname ());
}
}
//-----
//get text box reference
public void setText(TextBox tx) {
txKids = tx;
}
//-----
//clear lists and set buttons to disabled
public void clearClicked() {
//disable buttons and clear list
cpButton.Enabled = false;
clrButton.Enabled = false;
pkList.Items.Clear();
}
//-----
//copy data from text box to list box
public void copyClicked() {
//copy name to picked list
pkList.Items.Add(txKids.Text);
//clear button enabled
clrButton.Enabled = true;
klist.SelectedIndex = -1;
}
//-----
//copy selected kid to text box
//enable copy button
public void kidPicked() {
//copy text from list to textbox
txK ids.Text = klist.Text;
//copy button enabled
cpButton.Enabled = true;
}
}
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 carry out this initialization in the Mediator’s constructor, which sets all the controls 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 the following.
clearClicked(); //clear all controls
The two buttons in this program use command objects. Just as we noted earlier, this makes processing of the button click events quite simple.
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 25-5, illustrating both the Mediator pattern and the use of the Command pattern.
Figure 25-5. The interactions between the Command objects and the Mediator object
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 void action(MoveButton mv);
public void action(clrButton clr);
public void action(KidList klist);
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, but 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.