Mediator Design Pattern in Delphi
Object oriented design encourages the distribution of behaviour among objects. Such distribution can result in an object structure with many connections between objects; in the worst case, every object ends up knowing about every other.
Though partitioning a system into many objects generally enhances reusability, proliferating interconnections tend to reduce it again. Lots of interconnections make it less likely that an object can work without the support of others - the system acts as though it were monolithic. Moreover it can be difficult to change the systems' behaviour in any significant way, since behaviour is distributed among many objects. As a result, you may be forced to define many subclasses to customise the system's behaviour.
Developers at Borland probably were aware of the above mentioned paragraphs when designing the Delphi Object Pascal language and the VCL components. Their solution to this problem is essentially: events (or method pointers). The use of events makes de-coupling of objects possible. The class
TForm is their standard mediator class which handles (wires) events from components put on the form.
They even delivered a great tool with it: the Object Inspector. Being able to delegate behaviour to another class using events, saves you, for example, from subclassing
TButton when you need a button that interacts with an
TEdit control. The mediator is the form which handles all events and takes corresponding actions, coupling all related components together with snippets of code. Great. So what's the need for a Mediator pattern?
Delphi's Object Inspector uses RTTI and form designers to create event handlers on forms and Delphi's component streaming mechanism takes care of actually wiring these events at run time.
If you ever manually created event handlers you know that you have to:
- Create a event handler method with the correct parameter list and possibly the correct function result. Wire the event handler to the event with code like:
FSample.OnChange := SampleChange;
- Make sure you unwire the event again in appropriate cases to avoid wired objects from calling destroyed or otherwise illegal objects.
In order to construct a correct event handler method it needs the event type definition. The mediator pattern relies on these event type definitions to construct and update event handlers.
Suppose we've created a class
TComPort which encapsulates a serial port device driver. This class defines a series of events to asynchronously inform clients of state changes: data triggers, line errors etc. A
TController class could 'use a'
TComPort class to transmit data through a serial port.
TController class would typically wire and handle the
Comport events to update it's internal state. Here's an abstract of the interface and implementation of
TController without the mediator:
type TLineEvent = procedure (Sender: TObject; Error: TLineError) of object; TTriggerEvent = procedure (Sender: TObject; Trigger: TTrigger) of object; TComPort = class (TObject) published property OnDataReceived: TTriggerEvent read FOnDataReceived write FOnDataReceived; property OnLineEvent: TLineEvent read FOnLineEvent write FOnLineEvent; property OnTimeOut: TNotifyEvent read FOnTimeOut write FOnTimeOut; property OnTransmitted: TNotifyEvent read FOnTransmitted write FOnTransmitted; end; TController = class (TObject) private FComPort: TComPort; public constructor Create; destructor Destroy; override; property ComPort: TComPort read FComPort; end; implementation constructor TController.Create; begin inherited Create; FComPort := TComPort.Create; end; destructor TController.Destroy; begin FComPort.Free; inherited Destroy; end;
Now have a look at the same abstract for class
TController with the mediator applied to this structure:
TController = class (TObject) private FComPort: TComPort; protected procedure ComPortDataReceived(Sender: TObject; Trigger: TTrigger); procedure ComPortLineEvent(Sender: TObject; Error: TLineError); procedure ComPortTimeOut(Sender: TObject); procedure ComPortTransmitted(Sender: TObject); procedure UnwireComPort; procedure WireComPort; public constructor Create; destructor Destroy; override; property ComPort: TComPort read FComPort; end; implementation constructor TController.Create; begin inherited Create; FComPort := TComPort.Create; //now wire comport calling the method created by the mediator WireComport; end; destructor TController.Destroy; begin //make sure the comport is unwired again UnwireComport; FComPort.Free; inherited Destroy; end; procedure TController.ComPortDataReceived(Sender: TObject; Trigger: TTrigger); begin end; procedure TController.ComPortLineEvent(Sender: TObject; Error: TLineError); begin end; procedure TController.ComPortTimeOut(Sender: TObject); begin end; procedure TController.ComPortTransmitted(Sender: TObject); begin end; procedure TController.UnwireComPort; begin FComPort.OnDataReceived := nil; FComPort.OnLineEvent := nil; FComPort.OnTimeOut := nil; FComPort.OnTransmitted := nil; end; procedure TController.WireComPort; begin FComPort.OnDataReceived := ComPortDataReceived; FComPort.OnLineEvent := ComPortLineEvent; FComPort.OnTimeOut := ComPortTimeOut; FComPort.OnTransmitted := ComPortTransmitted; end;
In this example notice: The wiring of the comport in
constructor Create by a call to
WireComPort. This method is optionally created by the mediator. The corresponding unwiring in
destructor Destroy by a call to
UnwireComPort. This method is optionally created by the mediator. The event handler methods which all have the correct signature (as defined in the event library), just like the Object Inspector would do.