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.

Code examples