Chapter 25. The Mediator Pattern

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.

An Example System

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

image

When the program starts, the Copy and Clear buttons are disabled.

  1. 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.
  2. 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 25-2.

    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.

    image

  3. 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.

Interactions between Controls

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

image

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.

image

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.

Sample Code

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;
      }
}

Initialization of the System

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

Mediators and Command Objects

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

image

Consequences of the Mediator Pattern

  1. The Mediator pattern keeps classes from becoming entangled when actions in one class need to be reflected in the state of another class.
  2. 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.
  3. You can add new controls or other classes without changing anything except the Mediator.
  4. 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.
  5. 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 interaction between objects.
  6. 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.

Single Interface Mediators

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.

Implementation Issues

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.

Program on the CD-ROM

image

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset