Friday, April 10, 2020

Design Patterns - The Mediator

What is a mediator and how do I use it?

The mediator describes one solution to a key issue that we all encounter whenever we try to design a new class. How to deal with situations where one object has to respond to changes in, or control the behavior of, another.
How do I recognize where I need a mediator?
The formal definition of the mediator, as given by the “Design Patterns, Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides is:
Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
This addresses one of the most fundamental problems that we have to tackle in any software development task, that of ensuring that objects can communicate with each other without actually having to include hard-coded references in our classes.
Nowhere is this more critical than when building the complex user interfaces that the current generation of computer-literate end users not only expect, but demand. Typically we have to make the entire UI respond as a single entity, enabling and disabling functions and controls in response to the user’s actions and choices, or according to their rights and permissions. At the same time we want to design and build generic, reusable classes. The two requirements are, apparently, in direct conflict with one another.
Consider the problem of kinking a couple of textboxes in a form. When a value is entered into one, the other must display the result of some sort of calculation based on that value (e.g. When entering a price in one, display the Sales Tax in the other). Obviously, if there is no value in the source, there should be no result in the target, but how can we ensure that the calculation only gets done when a value greater than 0 is entered?
Of course, the simplest solution is just to add a couple of lines of code to the Valid() of the textbox that would call the necessary functionality like this:
IF This.Value > 0
   *** Calculate the tax display

   ThisForm.TxtTax.Value = ( This.Value * (5.75/100) )
ELSE   *** Clear the tax display
   ThisForm.TxtTax.Value = 0

ENDIF
This sort of ‘tight coupling’ may be acceptable when we are dealing with just a simple pair of controls in a single form. However, we will quickly run into trouble if we try to adopt this solution when dealing with multiple controls that have to interact in complex and differing combinations. Even finding where the code that controls a particular interaction is specified can be a problem, and simply changing the name of one object becomes a major undertaking. This is where the mediator pattern comes into its own.
The basic idea is that each object communicates with a central ‘mediator’ which knows about all of the objects that are currently in scope, and how to manage their state when a given event is reported. In this way we avoid all of the issues associated with placing specific code into the method associated with the Valid() event of a control. Instead we can write completely generic code in its parent class. So, if we were using a mediator object, we could replace the specific code in the instance of our price textbox with code like this in its parent class:
This.oMediator.Notify( This )
As you can see, the textbox has no idea what the mediator will do with the information, or even what information it wants. All that it has to do is to call the mediator’s ‘Notify’ method and pass a reference to itself. Any subsequent action is up to the specific implementation of the mediator.
What are the components of a mediator?
A mediator has two essential requirements. First we need a “mediator” class that defines the interface, and generic functionality, for the “concrete mediators” that are the sub classes that define the various possible implementations. The second is that all Colleague objects must be based on classes that can communicate with their mediator. The basic structure for the mediator pattern is shown below:


You can see from the diagram that classes that need to work with a mediator need to hold a reference to the mediator object. You are probably thinking that, in Visual FoxPro we have a ready-made candidate for the mediator class in the Form (in the context of the UI anyway). Using the form as the mediator is nice because it is always available to any object through the ‘ThisForm’ reference without the need to worry about specific properties. However, when dealing with non-visual classes this may not be the best solution, so it is probably better to define a generic, non-visual, mediator class.
The second thing that you will notice from the diagram is that the mediator object needs to hold a reference to all of its colleagues. This raises the question of how it should acquire and hold those references – to which there are several possible answers. Perhaps the simplest to implement is that each control class defines a ‘RegisterMe()’ method that, whenever an instance of the class is created checks for the presence of a mediator and passes a reference to itself to the mediator. Another possibility is that the mediator class defines a ‘GoFindEm()’ method which searches its environment to discover objects. Either way the mediator class will also need to maintain a collection (array) to store references to its colleagues.
How do I implement a mediator?
In order to implement a mediator for our simple little Tax example we have quite a lot of preparation to do. First we will need a form class that can handle a mediator. Such a class will need three custom properties to deal with the mediator:
·         cmedclass     Name of the Mediator Class to Instantiate for this form
·         cmedlib         Class Library for the Mediator Class to be instantiated for this form
·         omediator     Exposed property to hold the reference to the Mediator object
and has the following code added to its Load() event (We are using Load() because we must ensure that the defined Mediator is in place before any other form control gets instantiated. Init() would be too late because it does not fire until after all controls have been instantiated).

LOCAL lcMClass, lcMLib

DODEFAULT()
WITH ThisForm
   *** If a mediator class is specified, instantiate it
   lcMClass = ALLTRIM( .cMedClass )

   lcMLib = ALLTRIM( .cMedLib )
   IF NOT EMPTY( lcMClass ) AND NOT EMPTY( lcMLib )
      .oMediator = NEWOBJECT( lcMClass, lcMLib )
   ENDIF
ENDWITH

Of course, we also have to be careful about cleaning up object references, so the form class includes the following code in its Destroy() event, to make sure that the clean up happens

IF VARTYPE( This.oMediator ) = "O"
   This.oMediator.Destroy()ENDIF

Note: This is required to avoid the problem that arises because the normal sequence of events is that objects are destroyed in the reverse order of their creation. Since the mediator is created first it would normally be destroyed last, but because it holds references to other objects they cannot be destroyed while it exists. So we need to force the mediator’s destruction ‘out of turn’ when the form is being released so that the objects can release themselves properly.
Next we will need to define new sub classes for each of the controls that will have to work with the mediator. A new custom property (oMediator) has been defined to hold a local reference to the mediator object, together with three new methods;
·         Register ()        Called from the Init() of the control. Checks for the presence of a mediator. If one is found it stores a local reference to it and then calls mediator’s Register() method, passing a reference to itself
·         Notify()             Method that calls the state change notification method (also named “notify”) on the mediator and passes a reference to the current control
·         UnRegister()      Called from the Destroy() of the control. Checks the local reference to the mediator. If found, calls the mediator’s UnRegister() method, passing a reference to itself and then releases the local reference
In addition, the following events (where applicable) have to be modified to call the control’s Notify() method either InterActiveChange() and ProgrammaticChange() for lists or Valid() for text and edit boxes.
Next we need an abstract mediator class which defines the interface, and generic behavior for the Register() Notify() and Unregister() methods and has a RegisteredObjects collection. The code to add and remove objects to the collection is very simple indeed (I typically use the concatenation of an object’s “Parent + Name” properties as the key for items in the collection because there may be objects with the same name in different containers on a form) and the only real complexity is in the Notify() method.
The way I like to implement this is by defining custom methods in the concrete sub classes to handle the interactions for each object they deal with. The code in the mediator’s Notify() method can then be generic and simply checks to see if it has a method for the current object. Here is the code for that:

LPARAMETERS toObjRef

LOCAL lcKey, lcMethod, lnItem
*** Get the parent + name into a local Variable
lcKey = UPPER( ALLTRIM( toObjRef.Parent ))
lcKey = lcKey + UPPER( ALLTRIM( toObjRef.Name ))
*** And the target method will be based on the name
lcMethod = “CHANGE_” + UPPER( ALLTRIM( toObjRef.Name ))
*** Do we have this object registered
lnItem = This.RegisteredObjects.GetKey( lcKey )
IF lnItem > 0
   *** This is registered, do we have some action for it
   IF PEMSTATUS( This, lcMethod, 5 )
      *** Call the method and pass the reference to the original object
      EVALUATE( “This.” + lcMethod + “(  toObjRef )” )
   ENDIF
ENDIF
*** Just return from here
RETURN

We can then create a specific sub class of this to implement the specific behavior that we want in our form by implementing the custom methods that will be called from the Notify() method. For our (very) simple example the ChangeTxtPrice() method might look like this:

LPARAMETERS toCalledBy

LOCAL lcKey, lnItem, loTgt, lnVal
*** do we have a tax textbox?
lcKey = UPPER( ALLTRIM( toObjRef.Parent ))
lcKey = lcKey + "TXTTAX"
lnItem = This.RegisteredObjects.GetKey( lcKey )
IF lnItem > 0
   *** Yes, it is registered, so get a reference to it
   loTgt = This.RegisteredObjects.Item( lnItem )
   *** Get the source object's value
   lnVal = toObjRef.Value
   IF lnVal > 0
      loTgt.Value = (lnVal* (5.75/100))
 
  ELSE
      loTgt.Value = 0
   ENDIF
   loTgt.Refresh()
ENDIF
RETURN

There are, of course, many ways to implement the concrete mediator but the methodology outlined here illustrates the underlying principle. 
Finally we have to create a form using the new classes.
Phew! Seems like a lot of work doesn’t it  - especially since all we want to do is to update one textbox based on the value of another! However, you will notice that everything except the creation of the concrete mediator that implements the specific behavior is entirely generic and is, therefore, completely re-usable. In other words, it only has to be done once.
It is important to emphasize just how much flexibility we have gained by doing all this. All of the code that governs the interaction between any number of objects on a form is now confined to a single object which is specific to the form, but that can be maintained outside of it. To change the behavior we can either modify the existing sub class, or create a new one and implement it by just changing a couple of properties on the form.
Mediator pattern summary
Implementing the mediator pattern undoubtedly requires more planning, and much more preparation than any of the patterns we have discussed so far. However, in situations where you have to manage complex interactions, it amply repays the effort in three ways.
·         First, it simplifies maintenance by localizing behavior that would otherwise be spread among several classes in a single object
·         Second, it avoids the necessity for tight coupling between objects
·         Third, it simplifies the logic by replacing ‘many to many’ interactions between individual controls with ‘one to many’ interactions between the mediator and colleagues
The main drawback to the pattern is that, especially when dealing with large numbers of interactions, the mediator itself can get very complicated and difficult to maintain. 
Published Sunday, January 14, 2007 12:24 PM by andykr

No comments:

Post a Comment

Writing better code (Part 1)

Writing better code (Part 1) As we all know, Visual FoxPro provides an extremely rich and varied development environment but sometimes to...