Keywords

These keywords were added by machine and not by the authors. This process is experimental and the keywords may be updated as the learning algorithm improves.

The mediator pattern is used to simplify and rationalize the communication between groups of objects. This is one of the least well-known design patterns, but it solves a common problem and can simplify the design of an application significantly. Table 21-1 puts the mediator pattern into context.

Table 21-1. Putting the Mediator Pattern into Context

Preparing the Example Project

For this chapter I created an Xcode Command Line Tool project called Mediator and added to it a file called Airplane.swift, the contents of which are shown in Listing 21-1.

Listing 21-1. The Contents of the Airplane.swift File

struct Position {

var distanceFromRunway:Int;

var height:Int;

}

func == (lhs:Airplane, rhs:Airplane) -> Bool {

return lhs.name == rhs.name;

}

class Airplane : Equatable {

var name:String;

var currentPosition:Position;

private var otherPlanes:[Airplane];

init(name:String, initialPos:Position) {

self.name = name;

self.currentPosition = initialPos;

self.otherPlanes = [Airplane]();

}

func addPlanesInArea(planes:Airplane...) {

for plane in planes {

otherPlanes.append(plane);

}

}

func otherPlaneDidLand(plane:Airplane) {

if let index = find(otherPlanes, plane) {

otherPlanes.removeAtIndex(index);

}

}

func otherPlaneDidChangePosition(plane:Airplane) -> Bool {

return plane.currentPosition.distanceFromRunway

== self.currentPosition.distanceFromRunway

&& abs(plane.currentPosition.height

- self.currentPosition.height) < 1000;

}

func changePosition(newPosition:Position) {

self.currentPosition = newPosition;

for plane in otherPlanes {

if (plane.otherPlaneDidChangePosition(self)) {

println("\(name): Too close! Abort!");

return;

}

}

println("\(name): Position changed");

}

func land() {

self.currentPosition = Position(distanceFromRunway: 0, height: 0);

for plane in otherPlanes {

plane.otherPlaneDidLand(self);

}

println("\(name): Landed");

}

}

The Airplane class represents the state of an aircraft as it approaches an airport and tracks its current position using the Position struct. There may be other planes approaching the airport, so each Airplane objects keeps track of those around it and ensures that its movements will not bring it too close to another plane. Listing 21-2 shows the statements that I added to the main.swift file to create and manipulate an instance of the Airplane class.

Listing 21-2. The Contents of the main.swift File

// initial setup

let british = Airplane(name: "BA706", initialPos: Position(distanceFromRunway: 11, height: 21000));

// plane approaches airport

british.changePosition(Position(distanceFromRunway: 8, height: 10000));

british.changePosition(Position(distanceFromRunway: 2, height: 5000));

british.changePosition(Position(distanceFromRunway: 1, height: 1000));

// plane lands

british.land();

I create an Airplane object to represent a British Airways flight and then call the changePosition method several times to reflect its progress toward the airport and then call the land method. Running the example application produces the following output:

BA706: Position changed

BA706: Position changed

BA706: Position changed

BA706: Landed

Understanding the Problem That the Pattern Solves

The problem with the example application becomes apparent when there are several Airplane objects used to represent approaches to the airport. Listing 21-3 shows the changes that I made to the main.swift file to add two additional Airplane objects.

Listing 21-3. Using Additional Airplane Objects in the main.swift File

// initial setup

let british = Airplane(name: "BA706", initialPos:

Position(distanceFromRunway: 11, height: 21000));

// new plane arrives

let american = Airplane(name: "AA101", initialPos: Position(distanceFromRunway: 12, height: 22000));

british.addPlanesInArea(american);

american.addPlanesInArea(british);

// plane approaches airport

british.changePosition(Position(distanceFromRunway: 8, height: 10000));

british.changePosition(Position(distanceFromRunway: 2, height: 5000));

british.changePosition(Position(distanceFromRunway: 1, height: 1000));

// new plane arrives

let cathay = Airplane(name: "CX200", initialPos: Position(distanceFromRunway: 13, height: 22000));

british.addPlanesInArea(cathay);

american.addPlanesInArea(cathay);

cathay.addPlanesInArea(british, american);

// plane lands

british.land();

// plane moves too close

cathay.changePosition(Position(distanceFromRunway: 12, height: 22000));

Running the application produces the following output:

BA706: Position changed

BA706: Position changed

BA706: Position changed

BA706: Landed

CX200: Too close! Abort!

There are only three Airplane objects, but the complexity of the code in the main.swift file has increased sharply because each Airplane has to keep track of every other Airplane. Taking into account the amount of code required to keep track of the other aircraft inside the Airplane object, a substantial percentage of the application is given over just to managing the communication between Airplane objects. The result is a set of Airplane objects that know about each other and communicate by invoking methods directly on one another, as shown in Figure 21-1.

Figure 21-1.
figure 1figure 1

The peer communications problem

This is a problem that gets worse as the number of objects increases because every Airplane object has to be made aware of every other instance, creating an ever more complex set of dependencies in which it is easy to forget to establish a new connection, as shown in Figure 21-2.

Figure 21-2.
figure 2figure 2

The effect of forgetting to establish a new connection

This kind of omission is nasty because it manifests itself as a problem when a feature relies on the missing connection. In this case, the collision-avoidance code in the changePosition method will always work unless the Qantas flight tries to move into the space occupied by the American Airlines flight. If this happens, the position of the American Airlines flight won’t be checked, and collision-avoidance will have failed.

Understanding the Mediator Pattern

The mediator pattern solves the problem by introducing a mediator object that eases communication between two or more peer objects, often referred to as colleagues. The mediator keeps track of the peer objects and facilities the communication between them in order to break the dependencies between objects, avoid problems caused by omitted relationships, and simplify the overall application. Figure 21-3 illustrates the way that the mediator pattern transforms the Airplane problem.

Figure 21-3.
figure 3figure 3

The mediator pattern

Each Airplane has a relationship with the mediator, rather than with its peers. It sends its messages to the mediator, and the mediator keeps track of the other Airplane objects and forwards the messages to them. The mediator reduces the number of dependencies in the application and ensures that all of the messages are dispatched to all of the peers, avoiding the missed connection problem.

Implementing the Mediator Pattern

The core of the mediator pattern is a pair of protocols: one that defines the functionality provided by the peers and one that defines the functionality for the mediator. You can see how I have defined the protocols in Listing 21-4, which shows the contents of the Mediator.swift file that I added to the example project.

Listing 21-4. The Contents of the Mediator.swift File

protocol Peer {

var name:String {get};

func otherPlaneDidChangePosition(position:Position) -> Bool;

}

protocol Mediator {

func registerPeer(peer:Peer);

func unregisterPeer(peer:Peer);

func changePosition(peer:Peer, pos:Position) -> Bool;

}

The Peer protocol defines a name property for identification purposes and an otherPlaneDidChangePosition method that is called to check to see whether it is safe for another aircraft to move. The Mediator protocol defines registerPeer and unregisterPeer methods to add and remove objects from the set it mediates and defines a changePosition method that will call the otherPlanChangePosition method of the peers it mediates.

Defining the Meditator Class

The next step is to define a class that conforms to the Mediator protocol and that can be used to mediate the communication between a set of Peer objects. Listing 21-5 shows the class I defined.

Listing 21-5. Defining the Mediator Implementation in the Mediator.swift File

protocol Peer {

var name:String {get};

func otherPlaneDidChangePosition(position:Position) -> Bool;

}

protocol Mediator {

func registerPeer(peer:Peer);

func unregisterPeer(peer:Peer);

func changePosition(peer:Peer, pos:Position) -> Bool;

}

class AirplaneMediator : Mediator {

private var peers:[String:Peer];

init() {

peers = [String:Peer]();

}

func registerPeer(peer: Peer) {

self.peers[peer.name] = peer;

}

func unregisterPeer(peer: Peer) {

self.peers.removeValueForKey(peer.name);

}

func changePosition(peer:Peer, pos:Position) -> Bool {

for storedPeer in peers.values {

if (peer.name != storedPeer.name

&& storedPeer.otherPlaneDidChangePosition(pos)) {

return true;

}

}

return false;

}

}

The implementation of the AirplaneMediator class is simple. I store the collection of Peer objects using a dictionary, which eases the implementation of the changePosition methods that must ensure that the caller of the method doesn’t have its otherPlaneDidchangePosition method called for its own position change.

Conforming to the Peer Protocol

The next step is to update the Airplane class so that it conforms to the Peer protocol and no longer manages a list of its peers. Listing 21-6 shows the changes that I made.

Listing 21-6. Conforming to the Peer Protocol in the Airplane.swift File

struct Position {

var distanceFromRunway:Int;

var height:Int;

}

class Airplane : Peer {

var name:String;

var currentPosition:Position;

var mediator:Mediator;

init(name:String, initialPos:Position, mediator: Mediator) {

self.name = name;

self.currentPosition = initialPos;

self.mediator = mediator;

mediator.registerPeer(self);

}

func otherPlaneDidChangePosition(position:Position) -> Bool {

return position.distanceFromRunway

== self.currentPosition.distanceFromRunway

&& abs(position.height - self.currentPosition.height) < 1000;

}

func changePosition(newPosition:Position) {

self.currentPosition = newPosition;

if (mediator.changePosition(self, pos: self.currentPosition) == true) {

println("\(name): Too close! Abort!");

return;

}

println("\(name): Position changed");

}

func land() {

self.currentPosition = Position(distanceFromRunway: 0, height: 0);

mediator.unregisterPeer(self);

println("\(name): Landed");

}

}

The overall effect is to focus the class on its own state and rely on the meditator to manage the relationship with other peers on its behalf. Listing 21-7 shows how I updated the code in the main.swift file to use the mediator.

Listing 21-7. Using the Mediator in the main.swift File

let mediator:Mediator = AirplaneMediator();

// initial setup

let british = Airplane(name: "BA706", initialPos:

Position(distanceFromRunway: 11, height: 21000), mediator:mediator);

// new plane arrives

let american = Airplane(name: "AA101", initialPos: Position(distanceFromRunway: 12, height: 22000), mediator:mediator);

// plane approaches airport

british.changePosition(Position(distanceFromRunway: 8, height: 10000));

british.changePosition(Position(distanceFromRunway: 2, height: 5000));

british.changePosition(Position(distanceFromRunway: 1, height: 1000));

// new plane arrives

let cathay = Airplane(name: "CX200", initialPos: Position(distanceFromRunway: 13, height: 22000), mediator:mediator);

// plane lands

british.land();

// plane moves too close

cathay.changePosition(Position(distanceFromRunway: 12, height: 22000));

I no longer need to notify each Airplane when I create another instance because the mediator will automatically keep track for me and ensure that I don’t forget to create all of the connections required before the mediator was applied. Running the application produces the following output:

BA706: Position changed

BA706: Position changed

BA706: Position changed

BA706: Landed

CX200: Too close! Abort!

Implementing Concurrency Protections

Like with many of the patterns I describe in this book, implementing the mediator pattern means considering whether the peer objects will need to communicate with each other concurrently or whether peers will be registered or unregistered simultaneously. This won’t be the case in all applications, but if there is likely to be concurrent use, then concurrency protections are required, as described in the following sections.

Implementing Concurrency Protections in the Mediator

Concurrency protection in the mediator ensures that the collection of peers isn’t corrupted and that results returned by the mediator’s methods are consistent. Listing 21-8 shows how I have used Grand Central Dispatch (GCD) to protect the mediator class.

Listing 21-8. Implementing Concurrency Protections in the Mediator.swift File

import Foundation;

protocol Peer {

var name:String {get};

func otherPlaneDidChangePosition(position:Position) -> Bool;

}

protocol Mediator {

func registerPeer(peer:Peer);

func unregisterPeer(peer:Peer);

func changePosition(peer:Peer, pos:Position) -> Bool;

}

class AirplaneMediator : Mediator {

private var peers:[String:Peer];

private let queue = dispatch_queue_create("dictQ", DISPATCH_QUEUE_CONCURRENT);

init() {

peers = [String:Peer]();

}

func registerPeer(peer: Peer) {

dispatch_barrier_sync(self.queue, { () in

self.peers[peer.name] = peer;

});

}

func unregisterPeer(peer: Peer) {

dispatch_barrier_sync(self.queue, { () in

let removed = self.peers.removeValueForKey(peer.name);

});

}

func changePosition(peer:Peer, pos:Position) -> Bool {

var result = false;

dispatch_sync(self.queue, { () in

for storedPeer in self.peers.values {

if (peer.name != storedPeer.name

&& storedPeer.otherPlaneDidChangePosition(pos)) {

result = true;

}

}

});

return result;

}

}

I want to allow multiple operations to read the data in the peers dictionary unless I am about to modify it. I have used a concurrent GCD queue with a call to the dispatch_ syncfunction for read operations and with calls to the dispatch_barrier_ syncfunction in the registerPeer and unregisterPeer method to gain exclusive access to the dictionary for making changes.

Tip

Notice that I have assigned the result of the call to the removeValueForKey method to a constant in the implementation of the unregisterPeer method. Swift tries to be helpful and takes the result returned from the call to the dictionary method as the result to return from the closure—which is a problem because closures used as GCD blocks may not return results. Assigning the result to a constant captures the value and prevents it from being treated as a result.

Implementing Concurrency Protections in the Peer

The concurrency protections I added to the mediator do not make any assumptions about the implementation of the peer objects and allow multiple simultaneous calls to the otherPlanDidChangePosition method. This means I need to modify the Airplane class so as to protect the integrity of its internal state data, as shown in Listing 21-9.

Listing 21-9. Adding Concurrency Protections in the Airplane.swift File

import Foundation

struct Position {

var distanceFromRunway:Int;

var height:Int;

}

class Airplane : Peer {

var name:String;

var currentPosition:Position;

var mediator:Mediator;

let queue = dispatch_queue_create("posQ", DISPATCH_QUEUE_CONCURRENT);

init(name:String, initialPos:Position, mediator: Mediator) {

self.name = name;

self.currentPosition = initialPos;

self.mediator = mediator;

mediator.registerPeer(self);

}

func otherPlaneDidChangePosition(position:Position) -> Bool {

var result = false;

dispatch_sync(self.queue, {() in

result = position.distanceFromRunway

== self.currentPosition.distanceFromRunway

&& abs(position.height - self.currentPosition.height) < 1000;

});

return result;

}

func changePosition(newPosition:Position) {

dispatch_barrier_sync(self.queue, {() in

self.currentPosition = newPosition;

if (self.mediator.changePosition(self, pos:

self.currentPosition) == true) {

println("\(self.name): Too close! Abort!");

return;

}

println("\(self.name): Position changed");

});

}

func land() {

dispatch_barrier_sync(self.queue, { () in

self.currentPosition = Position(distanceFromRunway: 0, height: 0);

self.mediator.unregisterPeer(self);

println("\(self.name): Landed");

});

}

}

The concurrency protection that I added to the class allows multiple concurrent calls to the otherPlaneDidChangePosition method, but calls to the changePosition and land methods use a barrier to ensure that they have exclusive access to make modifications.

Variations on the Mediator Pattern

The standard implementation of the mediator pattern is focused on managing the relationships with peers, but common variations extend the role of the mediator, as I describe in the following sections.

Putting More Logic Into the Mediator

The first variation is to add logic into the mediator implementation class to more actively manage the flow of messages between peers or to provide additional features. To demonstrate this variation, I am going to reduce the number of peer objects that are called by the mediator changePosition method by adding some basic logic to filter out those that are farther away from the airport than the airplane that wants to be (on the basis that all of the planes are trying to land and are moving in the same direction). The first step is to expand the information revealed by the Peer protocol so that the mediator can access its location data, as shown in Listing 21-10.

Listing 21-10. Revealing Additional Information in the Mediator.swift File

...

protocol Peer {

var name:String {get};

var currentPosition:Position {get};

func otherPlaneDidChangePosition(position:Position) -> Bool;

}

...

Exposing the currentPosition property allows the mediator to be more selective about the peers whose methods it invokes, as shown in Listing 21-11.

Listing 21-11. Selectively Targeting Peers in the Mediator.swift File

...

func changePosition(peer:Peer, pos:Position) -> Bool {

var result = false;

dispatch_sync(self.queue, { () in

let closerPeers = self.peers.values.filter({p in

return p.currentPosition.distanceFromRunway

<= pos.distanceFromRunway;

});

for storedPeer in closerPeers {

if (peer.name != storedPeer.name

&& storedPeer.otherPlaneDidChangePosition(pos)) {

result = true;

}

}

});

return result;

}

...

I use the filter method to eliminate those planes that are farther away and then invoke the otherPlaneDidChangePosition method on the remaining objects. Running the application produces the following output, which is the same as previous examples:

BA706: Position changed

BA706: Position changed

BA706: Position changed

BA706: Landed

CX200: Too close! Abort!

The benefit of this variation is that I make fewer calls to peer objects, and in doing so, I hope to speed up the process of changing the position of an airplane. The drawback of this approach is that I have now codified a behavior into the mediator that will need to be changed if the application is extended to include planes that are departing from the airport and not just those that are arriving.

Generalizing the Mediator-Peer Relationship

The standard implementation of the mediator pattern means that the mediator has some knowledge into the methods defined by the peers in order that it can call those methods as needed. This makes it difficult to reuse a mediator class for a different set of peers.

If you expect to need multiple mediators in an application, a common variation is to generalize the implementation of the pattern and create a mediator class that doesn’t require any knowledge of the peers it is used with. There are two broad approaches to addressing this problem, although, as I explain in the following sections, both have their limitations.

Generalizing the Mediator with the Command Pattern

One approach is to combine the mediator pattern with the command pattern and have the mediator play the role of the invoker I described in Chapter 20. Listing 21-12 shows how I have defined a generalized command-based mediator class in a file called CommandMediator.swift that I added to the example project.

Listing 21-12. Defining a Generalized Mediator Class in the CommandMediator.swift File

protocol CommandPeer {

var name:String { get };

}

class Command {

let function:CommandPeer -> Any;

init(function:CommandPeer -> Any) {

self.function = function;

}

func execute(peer:CommandPeer) -> Any {

return function(peer);

}

}

class CommandMediator {

private var peers = [String:CommandPeer]();

func registerPeer(peer:CommandPeer) {

peers[peer.name] = peer;

}

func unregisterPeer(peer:CommandPeer) {

peers.removeValueForKey(peer.name);

}

func dispatchCommand(caller:CommandPeer, command:Command) -> [Any] {

var results = [Any]();

for peer in peers.values {

if (peer.name != caller.name) {

results.append(command.execute(peer));

}

}

return results;

}

}

Peers must conform to the CommandPeer protocol, in part to aid the implementation of the Command class and in part so that I can use the name property to prevent the mediator from executing a command on the object that created it.

Caution

For simplicity I have implemented the CommandMediator class without concurrency protections. Using the command pattern doesn’t protect the collection of peers, and you should apply protections to all mediators if concurrent use is possible. See the “Implementing Concurrency Protections” section for details.

The Command class represents the command that each peer will be asked to execute. As you saw in Chapter 20, there are different ways to arrange the definition and execution of a command, and the one that I have chosen to use means that the mediator will invoke the command, passing each peer into the execute method of the Command object. I have done this because I want to capture a result value from executing the command and present an array of those results to the calling peer.

The CommandMediator class is a variation of the mediator I used in the standard implementation of the pattern that presents a dispatchCommand method that accepts a Command object and passes each CommandPeer to its function. The results from executing the command on each peer are collected into an array and returned as the result to the calling peer.

Listing 21-13 shows how I updated the Airplane class to use the command-based mediator.

Listing 21-13. Using the CommandMediator in the Airplane.swift File

import Foundation

struct Position {

var distanceFromRunway:Int;

var height:Int;

}

class Airplane : CommandPeer {

var name:String;

var currentPosition:Position;

var mediator:CommandMediator;

let queue = dispatch_queue_create("posQ", DISPATCH_QUEUE_CONCURRENT);

init(name:String, initialPos:Position, mediator: CommandMediator) {

self.name = name;

self.currentPosition = initialPos;

self.mediator = mediator;

mediator.registerPeer(self);

}

func otherPlaneDidChangePosition(position:Position) -> Bool {

var result = false;

dispatch_sync(self.queue, {() in

result = position.distanceFromRunway

== self.currentPosition.distanceFromRunway

&& abs(position.height - self.currentPosition.height) < 1000;

});

return result;

}

func changePosition(newPosition:Position) {

dispatch_barrier_sync(self.queue, {() in

self.currentPosition = newPosition;

let c = Command(function: {peer in

if let plane = peer as? Airplane {

return plane.otherPlaneDidChangePosition (self.currentPosition);

} else {

fatalError("Type mismatch");

}

});

let allResults = self.mediator.dispatchCommand(self, command: c);

for result in allResults {

if result as? Bool == true {

println("\(self.name): Too close! Abort!");

return;

}

}

println("\(self.name): Position changed");

});

}

func land() {

dispatch_barrier_sync(self.queue, { () in

self.currentPosition = Position(distanceFromRunway: 0, height: 0);

self.mediator.unregisterPeer(self);

println("\(self.name): Landed");

});

}

}

This approach creates a mediator that can handle any group of objects instantiated from a class that conforms to the CommandPeer protocol, but some caution is required because the Command objects that a peer creates have to make an assumption about the type of the peers against which the command will be executed. Since any peer can send a command, this means that all peers have to be derived from the same base class and that you cannot use the CommandMediator class to mediate between objects of differing types, even if they are all instantiated from classes that conform to the CommandPeer protocol.

The implementation of the changePosition method in the Airplane class creates a command that casts peer objects to the Airplane type and calls the otherPlaneDidChangePosition method. I call the global fatalError function if the peer cannot be cast as an Airplane because the behavior of a mediator in this situation is undefined.

The final change is to the main.swift file, in which I must create an instance of the CommandMediator class, as shown in Listing 21-14.

Listing 21-14. Using the CommandMediator Class in the main.swift File

let mediator = CommandMediator();

// initial setup

let british = Airplane(name: "BA706", initialPos:

Position(distanceFromRunway: 11, height: 21000), mediator:mediator);

// new plane arrives

let american = Airplane(name: "AA101", initialPos:

Position(distanceFromRunway: 12, height: 22000), mediator:mediator);

// plane approaches airport

british.changePosition(Position(distanceFromRunway: 8, height: 10000));

british.changePosition(Position(distanceFromRunway: 2, height: 5000));

british.changePosition(Position(distanceFromRunway: 1, height: 1000));

// new plane arrives

let cathay = Airplane(name: "CX200", initialPos:

Position(distanceFromRunway: 13, height: 22000), mediator:mediator);

// plane lands

british.land();

// plane moves too close

cathay.changePosition(Position(distanceFromRunway: 12, height: 22000));

You can run the application to test that the changes don’t affect the output, which is as follows:

BA706: Position changed

BA706: Position changed

BA706: Position changed

BA706: Landed

CX200: Too close! Abort!

Generalizing the Mediator with Messages

An alternative approach is to target a single method on peer objects and provide sufficient information to let the peer work out what response is needed. This avoids the assumptions about the type of the peer objects and allows peers of different types to be used with a single mediator, but it means that all peers need to have the same understanding about the range of messages that will be sent, which presents its own problems. Listing 21-15 shows how I have defined the protocol and implementation class required for the message-based mediator in a new file called MessageMediator.swift that I added to the example project.

Listing 21-15. The Contents of the MessageMediator.swift File

protocol MessagePeer {

var name:String { get };

func handleMessage(messageType:String, data:Any?) -> Any?;

}

class MessageMediator {

private var peers = [String:MessagePeer]();

func registerPeer(peer:MessagePeer) {

peers[peer.name] = peer;

}

func unregisterPeer(peer:MessagePeer) {

peers.removeValueForKey(peer.name);

}

func sendMessage(caller:MessagePeer, messageType:String, data:Any) -> [Any?] {

var results = [Any?]();

for peer in peers.values {

if (peer.name != caller.name) {

results.append(peer.handleMessage(messageType, data: data));

}

}

return results;

}

}

The MessagePeer protocol defines a name property so that the mediator can identify the sender of a message and defines a handleMessage method that is passed a string describing the message type and an optional Any data value, which will be used to provide data about the message to peers. The MessageMediator class keeps track of peers and also defines a sendMessage method that peers call to dispatch a message to their counterparts. The mediator gathers the set of results from the peers and returns them in an array to the caller. Listing 21-16 shows how I changed the implementation of the Airplane class to use the message-based mediator.

Listing 21-16. Using the Message-Based Mediator in the Airplane.swift File

import Foundation

struct Position {

var distanceFromRunway:Int;

var height:Int;

}

class Airplane : MessagePeer {

var name:String;

var currentPosition:Position;

var mediator:MessageMediator;

let queue = dispatch_queue_create("posQ", DISPATCH_QUEUE_CONCURRENT);

init(name:String, initialPos:Position, mediator: MessageMediator) {

self.name = name;

self.currentPosition = initialPos;

self.mediator = mediator;

mediator.registerPeer(self);

}

func handleMessage(messageType: String, data: Any?) -> Any? {

var result:Any?;

switch (messageType) {

case "changePos":

if let pos = data as? Position {

result = otherPlaneDidChangePosition(pos);

}

default:

fatalError("Unknown message type");

}

return result;

}

func otherPlaneDidChangePosition(position:Position) -> Bool {

var result = false;

dispatch_sync(self.queue, {() in

result = position.distanceFromRunway

== self.currentPosition.distanceFromRunway

&& abs(position.height - self.currentPosition.height) < 1000;

});

return result;

}

func changePosition(newPosition:Position) {

dispatch_barrier_sync(self.queue, {() in

self.currentPosition = newPosition;

let allResults = self.mediator.sendMessage(self,

messageType: "changePos", data: newPosition);

for result in allResults {

if result as? Bool == true {

println("\(self.name): Too close! Abort!");

return;

}

}

println("\(self.name): Position changed");

});

}

func land() {

dispatch_barrier_sync(self.queue, { () in

self.currentPosition = Position(distanceFromRunway: 0, height: 0);

self.mediator.unregisterPeer(self);

println("\(self.name): Landed");

});

}

}

The advantage of this approach is that the Airplane class doesn’t need to make any assumptions about the other peers—but this comes at the cost of needing to ensure that all peers know about the same set of message types and respond to them in a consistent and useful manner. This is harder than it sounds in a complex application, and it is easy to have multiple peer types whose message handling drifts apart as the application becomes more complex. To complete this implementation, I need to use the MessageMediator class in the main.swift file, as shown in Listing 21-17.

Listing 21-17. Using the MessageMediator Class in the main.swift File

let mediator = MessageMediator();

// initial setup

let british = Airplane(name: "BA706", initialPos:

Position(distanceFromRunway: 11, height: 21000), mediator:mediator);

// new plane arrives

let american = Airplane(name: "AA101", initialPos:

Position(distanceFromRunway: 12, height: 22000), mediator:mediator);

// plane approaches airport

british.changePosition(Position(distanceFromRunway: 8, height: 10000));

british.changePosition(Position(distanceFromRunway: 2, height: 5000));

british.changePosition(Position(distanceFromRunway: 1, height: 1000));

// new plane arrives

let cathay = Airplane(name: "CX200", initialPos:

Position(distanceFromRunway: 13, height: 22000), mediator:mediator);

// plane lands

british.land();

// plane moves too close

cathay.changePosition(Position(distanceFromRunway: 12, height: 22000));

Understanding the Pitfalls of the Mediator Pattern

The most important pitfall to avoid is revealing details of one peer to another. The mediator should keep its peer collection private and not allow peers to locate or depend on one another, other than indirectly through the mediator. If you are implementing methods that return result, for example, then you must ensure that the peers do not return self as the result or, if they do, that the mediator doesn’t pass the reference back to the calling peer. Programmers like shortcuts, and allowing direct peer-to-peer contact allows the lazy programmer to bypass the mediator and undermine the pattern implementation.

The Single Protocol Pitfall

A common pitfall is to make the peers and the mediator conform to a common protocol, such that the peers do not know that a mediator is being used, believing instead that they are dealing with a single peer object. This may seem like a clever idea, but it ends in messy and confusing code because there is rarely a one-to-one mapping of the methods that the mediator provides to the methods that must be exposed by the peers. My advice is to use separate protocols for the peers and the mediator, which ensures that implementation classes don’t have to implement phantom methods or create tortured mappings between methods.

Examples of the Mediator Pattern in Cocoa

The Foundation framework includes a ready-made mediator called NSNotificationCenter that can be used to send notifications between objects. The NSNotificationCenterclass is a message-based mediator; the class allows peers to specify the kinds of messages they want to receive and to restrict the peers from which messages can originate—but there is no support for receiving responses from peers. Messages flow in one direction only. I created an Xcode playground called Notfications.playground to demonstrate the use of the NSNotificationCenter class, as shown in Listing 21-18.

Listing 21-18. The Contents of the Notifications.playground File

import Foundation;

let notifier = NSNotificationCenter.defaultCenter();

@objc class NotificationPeer {

let name:String;

init(name:String) {

self.name = name;

NSNotificationCenter.defaultCenter().addObserver(self,

selector: "receiveMessage:", name: "message", object: nil);

}

func sendMessage(message:String) {

NSNotificationCenter.defaultCenter().postNotificationName("message",

object: message);

}

func receiveMessage(notification:NSNotification) {

println("Peer \(name) received message: \(notification.object)");

}

}

let p1 = NotificationPeer(name: "peer1");

let p2 = NotificationPeer(name: "peer2");

let p3 = NotificationPeer(name: "peer3");

let p4 = NotificationPeer(name: "peer4");

p3.sendMessage("Hello!");

Note

The NSNotificationCenter class also implements the observer pattern, which I describe in Chapter 22.

An instance of the NSNotificationCenter class is obtained through the class defaultCenter property and is used both to register peers to receive and send messages. Registration is performed using the addObserver method, like this:

...

NSNotificationCenter.defaultCenter(). addObserver(self,

selector: "receiveMessage:", name: "message", object: nil) ;

...

The first argument is the object that messages will be sent to, for which I have specified the current object. The selector argument specifies the method that the messages will be sent to, expressed as an Objective-C style selector, which means that the method name should be followed with a colon character. The name argument is used to select only messages that are sent using a specific label, and the object argument can be used to limit messages to those sent from a specific source. I have set the name to message and object arguments to nil, indicating that I want to receive all of the messages with the label message, regardless of the label and source.

Tip

The method that is specified by the selector argument must be annotated with the @ objcattribute or contained within a class that is annotated with @objc.

Messages are sent using the postNotificationName method, specifying a label and an object that will be sent to the peers, like this:

...

NSNotificationCenter.defaultCenter()

        .postNotificationName("message", object: message);

...

In the playground, I define a class called NotificationPeer that calls the addObserver method to register for messages to be sent to the receiveMessage method, and I use the sendMessage method to send messages via the NSNotificationCenter, using the label message.

When you register to receive messages, the method specified by the selector argument must take a single argument of the NSNotification type, which is used to represent messages and which defines the properties shown in Table 21-2.

Table 21-2. The Properties Defined by the NSNotification Class

I create four NotificationPeer objects in the playground and then call the sendMessage method on one of them, which produces the following output in the console:

Peer peer1 received message: Optional(Hello!)

Peer peer2 received message: Optional(Hello!)

Peer peer3 received message: Optional(Hello!)

Peer peer4 received message: Optional(Hello!)

The NSNotificationCenter can be a useful class, but I find not being able to obtain responses from peers to be a limitation in many projects, so I generally implement my own alternative, as described in the “Variations on the Mediator Pattern” section of this chapter.

Applying the Pattern to the SportsStore Application

There isn’t a ready example for applying the mediator pattern on its own to the SportsStore for this chapter. Instead, in Chapter 22, I demonstrate how to use the mediator pattern and the command pattern together, which is a common combination.

Summary

In this chapter I described the mediator pattern and explained how it is used to handle communication between peer objects in order to reduce the complexity of an application and to ensure that no peer objects are left out. In the next chapter, I describe the observer pattern, which is used when an object needs to notify other objects when something interesting happens.