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.
Implementation
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.
This 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 TComport
and 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.