Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

19. Mediator Pattern
Written by Joshua Greene

The mediator pattern is a behavioral design pattern that encapsulates how objects communicate with one another. It involves four types:

  1. The colleagues are the objects that want to communicate with each other. They implement the colleague protocol.

  2. The colleague protocol defines methods and properties that each colleague must implement.

  3. The mediator is the object that controls the communication of the colleagues. It implements the mediator protocol.

  4. The mediator protocol defines methods and properties that the mediator must implement.

Each colleague contains a reference to the mediator, via the mediator protocol. In lieu of interacting with other colleagues directly, each colleague communicates through the mediator.

The mediator facilitates colleague-to-colleague interaction: Colleagues may both send and receive messages from the mediator.

When should you use it?

This mediator pattern is useful to separate interactions between colleagues into an object, the mediator.

This pattern is especially useful when you need one or more colleagues to act upon events initiated by another colleague, and, in turn, have this colleague generate further events that affect other colleagues.

Playground example

Open AdvancedDesignPattern.xcworkspace in the Starter directory, or continue from your own playground workspace you’ve been continuing to work on throughout the book, and then open the Mediator page from the File hierarchy.

Before you can write the Code example for this page, you need to create a base Mediator class.

Note: You can technically implement the mediator pattern without using a base Mediator, but if you do, you’ll likely write a lot more boilerplate code.

If you worked through Chapter 16, “MulticastDelegate Pattern,” you may notice that the Mediator class is similar to the MulticastDelegate class, but it has a few key differences that make it unique.

Under Sources, open Mediator.swift and add the following code:

// 1
open class Mediator<ColleagueType> {

  // 2
  private class ColleagueWrapper {
    var strongColleague: AnyObject?
    weak var weakColleague: AnyObject?

    // 3
    var colleague: ColleagueType? {
      return 
        (weakColleague ?? strongColleague) as? ColleagueType
    }

    // 4
    init(weakColleague: ColleagueType) {
      self.strongColleague = nil
      self.weakColleague = weakColleague as AnyObject
    }

    init(strongColleague: ColleagueType) {
      self.strongColleague = strongColleague  as AnyObject
      self.weakColleague = nil
    }
  }
}

Here’s what’s going on in this code:

  1. First, you define Mediator as a generic class that accepts any ColleagueType as the generic type. You also declare Mediator as open to enable classes in other modules to subclass it.

  2. Next, you define ColleagueWrapper as an inner class, and you declare two stored properties on it: strongColleague and weakColleague. In some use cases, you’ll want Mediator to retain colleagues, but in others, you won’t want this. Hence, you declare both weak and strong properties to support both scenarios.

    Unfortunately, Swift doesn’t provide a way to limit generic type parameters to class protocols only. Consequently, you declare strongColleague and weakColleague to be of type AnyObject? instead of ColleagueType?.

  3. Next, you declare colleague as a computed property. This is a convenience property that first attempts to unwrap weakColleague and, if that’s nil, then it attempts to unwrap strongColleague.

  4. Finally, you declare two designated initializers, init(weakColleague:) and init(strongColleague:), for setting either weakColleague or strongColleague.

Next, add the following code after the closing curly brace for ColleagueWrapper:

// MARK: - Instance Properties
// 1
private var colleagueWrappers: [ColleagueWrapper] = []

// 2
public var colleagues: [ColleagueType] {
  var colleagues: [ColleagueType] = []
  colleagueWrappers = colleagueWrappers.filter {
    guard let colleague = $0.colleague else { return false }
    colleagues.append(colleague)
    return true
  }
  return colleagues
}

// MARK: - Object Lifecycle
// 3
public init() { }

Taking each commented section in turn:

  1. First, you declare colleagueWrappers to hold onto the ColleagueWrapper instances, which will be created under the hood by Mediator from colleagues passed to it.

  2. Next, you add a computed property for colleagues. This uses filter to find colleagues from colleagueWrappers that have already been released and then returns an array of definitely non-nil colleagues.

  3. Finally, you declare init(), which will act as the public designated initializer for Mediator.

You also need a means to add and remove colleagues. Add the following instance methods after the previous code to do this:

// MARK: - Colleague Management
// 1
public func addColleague(_ colleague: ColleagueType,
                         strongReference: Bool = true) {
  let wrapper: ColleagueWrapper
  if strongReference {
    wrapper = ColleagueWrapper(strongColleague: colleague)
  } else {
    wrapper = ColleagueWrapper(weakColleague: colleague)
  }
  colleagueWrappers.append(wrapper)
}

// 2
public func removeColleague(_ colleague: ColleagueType) {
  guard let index = colleagues.firstIndex(where: {
    ($0 as AnyObject) === (colleague as AnyObject)
  }) else { return }
  colleagueWrappers.remove(at: index)
}

Here’s what this code does:

  1. As its name implies, you’ll use addColleague(_:strongReference:) to add a colleague. Internally, this creates a ColleagueWrapper that either strongly or weakly references colleague depending on whether strongReference is true or not.

  2. Likewise, you’ll use removeColleague to remove a colleague. In such, you first attempt to find the index for the ColleagueWrapper that matches the colleague using pointer equality, === instead of ==, so that it’s the exact ColleagueType object. If found, you remove the colleague wrapper at the given index.

Lastly, you need a means to actually invoke all of the colleagues. Add the following methods below removeColleague(_:):

public func invokeColleagues(closure: (ColleagueType) -> Void) {
  colleagues.forEach(closure)
}

public func invokeColleagues(by colleague: ColleagueType,
                             closure: (ColleagueType) -> Void) {
  colleagues.forEach {
    guard ($0 as AnyObject) !== (colleague as AnyObject)
      else { return }
    closure($0)
  }
}

Both of these methods iterate through colleagues, the computed property you defined before that automatically filters out nil instances, and call the passed-in closure on each colleague instance.

The only difference is invokeColleagues(by:closure:) does not call the passed-in closure on the matching colleague that’s passed in. This is very useful to prevent a colleague from acting upon changes or events that itself initiated.

You now have a very useful base Mediator class, and you’re ready to put this to good use!

Open the Mediator page from the File hierarchy, and enter this after Code example:

// MARK: - Colleague Protocol
public protocol Colleague: class {
  func colleague(_ colleague: Colleague?,
                 didSendMessage message: String)
}

You declare Colleague here, which requires conforming colleagues to implement a single method: colleague(_ colleague:didSendMessage:).

Next, add the following to the end of the playground:

// MARK: - Mediator Protocol
public protocol MediatorProtocol: class {
  func addColleague(_ colleague: Colleague)
  func sendMessage(_ message: String, by colleague: Colleague)
}

You declare MediatorProtocol here, which requires conforming mediators to implement two methods: addColleague(_:) and sendMessage(_:by:).

As you may have guessed from these protocols, you’ll create a mediator-colleague example where colleagues will send message strings via the mediator.

However, these won’t be just any colleagues — that wouldn’t be any fun. Instead, the colleagues will be the Three Musketeers: the legendary swordsmen Athos, Porthos and Aramis calling out battle cries to one another!

Okay, okay… maybe the example is a little silly, but it actually works really well! And, maybe, it will even help you remember the mediator pattern — “The mediator design pattern is the three musketeers calling each other!”

Enter the following code next; ignore the resulting compiler error for now:

// MARK: - Colleague
// 1
public class Musketeer {

  // 2
  public var name: String
  public weak var mediator: MediatorProtocol?

  // 3
  public init(mediator: MediatorProtocol, name: String) {
    self.mediator = mediator
    self.name = name
    mediator.addColleague(self)
  }

  // 4
  public func sendMessage(_ message: String) {
    print("\(name) sent: \(message)")
    mediator?.sendMessage(message, by: self)
  }
}

Let’s go over this step by step:

  1. You declare Musketeer here, which will act as the colleague.

  2. You create two properties, name and mediator.

  3. Within init, you set the properties and call mediator.addColleague(_:) to register this colleague; you’ll make Musketeer actually conform to Colleague next.

  4. Within sendMessage, you print out the name and passed-in message to the console and then call sendMessage(_:by:) on the mediator. Ideally, the mediator should then forward this message onto all of the other colleagues.

Next, add the following to the end of the playground:

extension Musketeer: Colleague {
  public func colleague(_ colleague: Colleague?,
                        didSendMessage message: String) {
    print("\(name) received: \(message)")
  }
}

Here, you make Musketeer conform to Colleague. To do so, you implement its required method colleague(_:didSendMessage:), where you print the Musketeer’s name and the received message.

You next need to implement the mediator. Add the following code next to do so:

// MARK: - Mediator
// 1
public class MusketeerMediator: Mediator<Colleague> {

}
extension MusketeerMediator: MediatorProtocol {

  // 2
  public func addColleague(_ colleague: Colleague) {
    self.addColleague(colleague, strongReference: true)
  }

  // 3
  public func sendMessage(_ message: String,
                          by colleague: Colleague) {
    invokeColleagues(by: colleague) {
      $0.colleague(colleague, didSendMessage: message)
    }
  }
}

Here’s what this does:

  1. You create MusketeerMediator as a subclass of Mediator<Colleague>, and you make this conform to MediatorProtocol via an extension.

  2. Within addColleague(_:), you call its super class’ method for adding a colleague, addColleague(_:strongReference:).

  3. Within sendMessage(_:by:), you call its super class’ method invokeColleagues(by:) to send the passed-in message to all colleagues except for the matching passed-in colleague.

This takes care of the required mediator classes, so you’re now ready to try them out! Add the following code next:

// MARK: - Example
let mediator = MusketeerMediator()
let athos = Musketeer(mediator: mediator, name: "Athos")
let porthos = Musketeer(mediator: mediator, name: "Porthos")
let aramis = Musketeer(mediator: mediator, name: "Aramis")

With the above, you declare an instance of MusketeerMediator called mediator and three instances of Musketeer, called athos, porthos and aramis.

Add the following code next to send some messages:

athos.sendMessage("One for all...!")
print("")

porthos.sendMessage("and all for one...!")
print("")

aramis.sendMessage("Unus pro omnibus, omnes pro uno!")
print("")

As a result, you should see the following printed to the console:

Athos sent: One for all...!
Porthos received: One for all...!
Aramis received: One for all...!

Porthos sent: and all for one...!
Athos received: and all for one...!
Aramis received: and all for one...!

Aramis sent: Unus pro omnibus, omnes pro uno!
Athos received: Unus pro omnibus, omnes pro uno!
Porthos received: Unus pro omnibus, omnes pro uno!

Note that the message senders do not receive the message. For example, the message sent by Athos was received by Porthos and Aramis, yet Athos did not receive it. This is exactly the behavior you’d expect to happen!

Using mediator directly, it’s also possible to send a message to all colleagues. Add following code to the end of the playground to do so:

mediator.invokeColleagues() {
  $0.colleague(nil, didSendMessage: "Charge!")
}

This results in the following printed to the console:

Athos received: Charge!
Porthos received: Charge!
Aramis received: Charge!

All of them get the message this time. Now let’s charge onwards with the project!

What should you be careful about?

This pattern is very useful in decoupling colleagues. Instead of colleagues interacting directly, each colleague communicates through the mediator.

However, you need to be careful about turning the mediator into a “god” object — an object that knows about every other object within a system.

If your mediator gets too big, consider breaking it up into multiple mediator–colleague systems. Alternatively, consider other patterns to break up the mediator, such as delegating some of its functionality.

Tutorial project

In this chapter, you’ll add functionality to an app called YetiDate. This app will help users plan a date that involves three different locations: a bar, restaurant and movie theater. It uses CocoaPods to pull in YelpAPI, a helper library for searching Yelp for said venues.

In the Starter directory, open YetiDate ▸ YetiDate.xcworkspace (not the .xcodeproj) in Xcode.

If you haven’t used CocoaPods before, that’s OK! Everything you need has been included for you in the starter project, so you don’t need to run pod install. The only thing you need to remember is to open YetiDate.xcworkspace, instead of the YetiDate.xcodeproj file.

Before you can run the app, you first need to register for a Yelp API key.

Registering for a Yelp API key

If you worked through CoffeeQuest in the Intermediate Section, you’ve already created a Yelp API key. You would have done this in Chapter 10, “Model-View-ViewModel Pattern”. Copy your existing key and paste it where indicated within APIKeys.swift, then skip the rest of this section and head to the “Creating required protocols” section.

If you didn’t work through CoffeeQuest, follow these instructions to generate a Yelp API key.

Navigate to this URL in your web browser:

Create an account if you don’t have one, or sign in. Next, enter the following in the Create App form (or if you’ve created an app before, use your existing API key):

  • App Name: Enter “Yeti Date”
  • App Website: Leave this blank
  • Industry: Select “Business”
  • Company: Leave this blank
  • Contact Email: Enter your email address
  • Description: Enter “Business search app”
  • I have read and accepted the Yelp API Terms: Check this

Your form should look as follows:

Press Create New App to continue, and you should see a success message:

Copy your API key and return to YetiDate.xcworkspace in Xcode.

Open APIKeys.swift from the File hierarchy, and paste your API key where indicated.

Creating required protocols

Since the app shows nearby restaurants, bars and movie theaters, it works best for areas with many businesses nearby. So the app’s default location has been set to San Francisco, California.

Note: You can change the location of the simulator by clicking Debug ▸ Location and then selecting a different option.

If you build and run the app, you’ll be prompted to grant permission to access your user’s location. Afterwards, however, you’ll see a blank map, and nothing happens!

Open PlanDateViewController.swift, which is the view controller that displays this map and conforms to MKMapViewDelegate to receive map-related events. Scroll down to mapView(_:didUpdate:), and you’ll find this call:

searchClient.update(userCoordinate: userLocation.coordinate)

This is what kicks off the process for searching for nearby businesses. Open SearchClient.swift, and you’ll see several methods have // TODO comments within them.

Here’s an overview of how the mediator-colleague system will work:

  • SearchColleague will act as the mediator. It will conform to SearchMediating and have strong references to SearchColleague objects.

  • YelpSearchColleague will act as the colleagues. It will conform to SearchColleague and have an unowned reference to the mediator via SearchMediating.

  • The files for SearchColleague, SearchColleagueMediating and YelpSearchColleague have already been added for you, but these are currently blank. It’s your job to implement them!

Firstly, open SearchColleague.swift and add the following:

import CoreLocation.CLLocation
import YelpAPI

// 1
public protocol SearchColleague: class {

  // 2
  var category: YelpCategory { get }
  var selectedBusiness: YLPBusiness? { get }

  // 3
  func update(userCoordinate: CLLocationCoordinate2D)

  // 4
  func fellowColleague(_ colleague: SearchColleague,
                       didSelect business: YLPBusiness)

  // 5
  func reset()
}

Here’s what this is about, step-by-step:

  1. First, you declare SearchColleague as a class protocol.

  2. Next, you define two properties: category will be the YelpCategory to search for, and selectedBusiness will be the YLPBusiness that has been selected.

    You should know that YelpAPI actually doesn’t define categories as an enum, but rather, it defines them as strings. To ensure correct string values are used, I’ve added YelpCategory to Yeti Date for you with valid strings for restaurants, bars and movie theaters and corresponding icon images.

  3. You’ll call update(userCoordinate:) to indicate that the user’s location has been updated.

  4. You’ll call fellowColleague(_ colleague: didSelect business:) to indicate to the other colleagues that the given colleague has selected a business.

  5. You’ll call reset() to remove any selectedBusiness, restore the SearchColleague to its initial search state and perform a new search.

Open SearchColleagueMediating.swift and add the following:

import YelpAPI

public protocol SearchColleagueMediating: class {

  // 1
  func searchColleague(
    _ searchColleague: SearchColleague,
    didSelect business: YLPBusiness)

  // 2
  func searchColleague(
    _ searchColleague: SearchColleague,
    didCreate viewModels: Set<BusinessMapViewModel>)

  // 3
  func searchColleague(
    _ searchColleague: SearchColleague,
    searchFailed error: Error?)
}

Here’s how you’ll use these methods:

  1. You’ll call searchColleague(_:didSelect:) whenever a SearchColleague has selected a business.

  2. You’ll call searchColleague(_:didCreate:) to indicate that the SearchColleague has created new view models that need to be displayed.

  3. You’ll call searchColleague(_:searchFailed:) to indicate that a SearchColleague has encountered a network error while searching.

Open YelpSearchColleague.swift and add this:

import CoreLocation
import YelpAPI

public class YelpSearchColleague {

  // 1
  public let category: YelpCategory
  public private(set) var selectedBusiness: YLPBusiness?

  // 2
  private var colleagueCoordinate: CLLocationCoordinate2D?
  private unowned let mediator: SearchColleagueMediating
  private var userCoordinate: CLLocationCoordinate2D?
  private let yelpClient: YLPClient

  // 3
  private static let defaultQueryLimit = UInt(20)
  private static let defaultQuerySort = YLPSortType.bestMatched
  private var queryLimit = defaultQueryLimit
  private var querySort = defaultQuerySort

  // 4
  public init(category: YelpCategory,
              mediator: SearchColleagueMediating) {
    self.category = category
    self.mediator = mediator
    self.yelpClient = YLPClient(apiKey: YelpAPIKey)
  }
}

Here’s what you’ve done:

  1. You declare two public properties: category and selectedBusiness.

  2. You create several private properties for performing searches: colleagueCoordinate, mediator, userCoordinate and yelpClient. YelpSearchColleague will use these to perform searches around either the user’s location, given by userCoordinate, or around another selected colleague’s business location, given by colleagueCoordinate.

  3. You declare private properties for limiting search results: queryLimit, which has a default value given by defaultQueryLimit, and querySort, which has a default value given by defaultQuerySort. You’ll see shortly how these are used.

  4. You declare the designated initializer, which accepts category and mediator.

Next, add the following to the end of the file:

// MARK: - SearchColleague
// 1
extension YelpSearchColleague: SearchColleague {

  // 2
  public func fellowColleague(_ colleague: SearchColleague,
                              didSelect business: YLPBusiness) {
    colleagueCoordinate = CLLocationCoordinate2D(
      business.location.coordinate)
    queryLimit /= 2
    querySort = .distance
    performSearch()
  }

  // 3
  public func update(userCoordinate: CLLocationCoordinate2D) {
    self.userCoordinate = userCoordinate
    performSearch()
  }

  // 4
  public func reset() {
    colleagueCoordinate = nil
    queryLimit = YelpSearchColleague.defaultQueryLimit
    querySort = YelpSearchColleague.defaultQuerySort
    selectedBusiness = nil
    performSearch()
  }

  private func performSearch() {
    // TODO
  }
}

Let’s go over this:

  1. You make YelpSearchColleague conform to SearchColleague, as intended per the design overview before.

  2. In response to receiving fellowColleague(_:didSelect:), you set the colleagueCoordinate, divide the queryLimit by two, change the querySort to .distance, and call performSearch() to do a new search.

    This results in a focused search around the colleagueCoordinate: You limit the results by reducing queryLimit and show the closest results by changing querySort to distance.

  3. In response to receiving update(userCoordinate:), you set self.userCoordinate and then perform a new search.

  4. In response to receiving reset(), you reset colleagueCoordinate, queryLimit, querySort and selectedBusiness to their default values and then perform a new search.

Next, replace the contents of performSearch() with the following:

// 1
guard selectedBusiness == nil,
  let coordinate = colleagueCoordinate ??
    userCoordinate else { return }

// 2
let yelpCoordinate = YLPCoordinate(
  latitude: coordinate.latitude,
  longitude: coordinate.longitude)
let query = YLPQuery(coordinate: yelpCoordinate)
query.categoryFilter = [category.rawValue]
query.limit = queryLimit
query.sort = querySort

yelpClient.search(with: query) {
  [weak self] (search, error) in
  guard let self = self else { return }
  guard let search = search else {
    // 3
    self.mediator.searchColleague(self,
                                  searchFailed: error)
    return
  }
  // 4
  var set: Set<BusinessMapViewModel> = []
  for business in search.businesses {
    guard let coordinate = business.location.coordinate
      else { continue }
    let viewModel = BusinessMapViewModel(
      business: business,
      coordinate: coordinate,
      primaryCategory: self.category,
      onSelect: { [weak self] business in
        guard let self = self else { return }
        self.selectedBusiness = business
        self.mediator.searchColleague(self,
                                      didSelect: business)
    })
    set.insert(viewModel)
  }

  // 5
  DispatchQueue.main.async {
    self.mediator.searchColleague(self, didCreate: set)
  }
}

This seems like a lot of work, but it’s actually not too difficult to understand.

  1. You first validate that selectedBusiness is nil and that there’s either a non-nil colleagueCoordinate or a non-nil userCoordinate. If either of these isn’t true, you return early.

  2. You then set up a YLPQuery and use this to query YLPClient.

  3. If there’s not a search object, then the Yelp API failed. If so, you inform the mediator and return early.

  4. You build up a Set<BusinessMapViewModel> by iterating through the search.businesses. BusinessMapViewModel conforms to MKAnnotation, which is exactly what’s needed to be displayed on the map.

  5. You dispatch to the main queue and notify the mediator that the view models were created by the YelpSearchColleague.

Great! This takes care of the colleagues, and you can now finish the mediator implementation.

Open SearchClient.swift and replace the class declaration with the following:

public class SearchClient: Mediator<SearchColleague> {

Here, you make SearchClient subclass Mediator<SearchColleague>, instead of NSObject.

Add the following code at the end of the file:

// MARK: - SearchColleagueMediating
// 1
extension SearchClient: SearchColleagueMediating {

  // 2
  public func searchColleague(
    _ searchColleague: SearchColleague,
    didSelect business: YLPBusiness) {

    delegate?.searchClient(self,
                           didSelect: business,
                           for: searchColleague.category)

    invokeColleagues(by: searchColleague) { colleague in
      colleague.fellowColleague(colleague, didSelect: business)
    }

    notifyDelegateIfAllBusinessesSelected()
  }

  private func notifyDelegateIfAllBusinessesSelected() {
    guard let delegate = delegate else { return }
    var categoryToBusiness: [YelpCategory : YLPBusiness] = [:]
    for colleague in colleagues {
      guard let business = colleague.selectedBusiness else {
        return
      }
      categoryToBusiness[colleague.category] = business
    }
    delegate.searchClient(
      self,
      didCompleteSelection: categoryToBusiness)
  }

  // 3
  public func searchColleague(
    _ searchColleague: SearchColleague,
    didCreate viewModels: Set<BusinessMapViewModel>) {

    delegate?.searchClient(self,
                           didCreate: viewModels,
                           for: searchColleague.category)
  }

  // 4
  public func searchColleague(
    _ searchColleague: SearchColleague,
    searchFailed error: Error?) {
    
    delegate?.searchClient(self,
                           failedFor: searchColleague.category,
                           error: error)
  }
}

Here’s what this does:

  1. You make SearchClient conform to SearchColleagueMediating via an extension.

  2. In response to searchColleague(_:didSelect:), you do the following: (i) Notify the delegate that a business was selected by the given colleague; (ii) Notify the other colleagues that a business was selected; and (iii) In the event that each of the colleagues has a selectedBusiness, you notify the delegate that selection has been completed.

  3. In response to searchColleague(_:didCreate:), you notify the delegate. In turn, the delegate is responsible for displaying these view models.

  4. Finally, in response to searchColleague(_:searchFailed:), you notify the delegate. In turn, the delegate is responsible for handling the error and/or retrying.

Just a few more methods to go! Replace the contents of setupColleagues() with the following:

let restaurantColleague = YelpSearchColleague(
  category: .restaurants, mediator: self)
addColleague(restaurantColleague)

let barColleague = YelpSearchColleague(
  category: .bars, mediator: self)
addColleague(barColleague)

let movieColleague = YelpSearchColleague(
  category: .movieTheaters, mediator: self)
addColleague(movieColleague)

With this code, you create YelpSearchColleagues for .restaurants, .bars and .movieTheaters categories.

Replace the contents of update(userCoordinate:) with the following:

invokeColleagues() { colleague in
  colleague.update(userCoordinate: userCoordinate)
}

In response to getting a new userCoordinate, you pass this along to each of the SearchColleague instances.

Lastly, replace the contents of reset() with the following:

invokeColleagues() { colleague in
  colleague.reset()
}

Likewise, you simply pass the reset() call onto each of the SearchColleague instances.

Whoo, that was a lot of work! Great job!

Build and run the app. The map should now show restaurants, bars and movie theaters.

Tap on an icon, and you’ll see a callout with a green checkmark.

Upon tapping the checkmark, the related YelpSearchColleague will get its selectedBusiness set, communicate this to its mediator, trigger the other colleagues to do a new search and ultimately generate new view models to show on the map! Eventually once you’ve selected one of each business type, you’ll see a screen showing your choices.

Key points

You learned about the mediator pattern in this chapter. Here are its key points:

  • The mediator pattern encapsulates how objects communicate with one another. It involves four types: colleagues, a colleague protocol, a mediator, and a mediator protocol.

  • The colleagues are the objects that communicate; the colleague protocol defines methods and properties all colleagues must have; the mediator controls the communication of the colleagues; and the mediator protocol defines required methods and properties that the mediator must have.

  • In lieu of talking directly, colleagues hold onto and communicate through the mediator. The colleague protocol and mediator protocol helps prevent tight coupling between all objects involved.

Where to go from here?

You also created Yeti Dates in this chapter! This is a neat app, but there’s a lot more you can do with it:

  • YelpSearchClient isn’t very efficient with searches. You can improve this by using caching and only performing searches when absolutely required.

  • After selecting businesses for each YelpSearchClient, a “Review Date” page appears, but it’s very basic. There’s a lot you can do to improve this, such as giving the option to navigate to each address.

  • Why stop at just restaurants, bars and movie theaters? You could let users pick whichever categories they’re interested in grouping together.

Each of these are possible using the existing patterns that you’ve learned in this book. Feel free to continue building out Yeti Date as much as you like.

When you’re ready, continue onto the next chapter to learn about the composite design pattern.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.