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:
-
The colleagues are the objects that want to communicate with each other. They implement the colleague protocol.
-
The colleague protocol defines methods and properties that each colleague must implement.
-
The mediator is the object that controls the communication of the colleagues. It implements the mediator protocol.
-
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
Mediatorclass is similar to theMulticastDelegateclass, 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:
-
First, you define
Mediatoras a generic class that accepts anyColleagueTypeas the generic type. You also declareMediatorasopento enable classes in other modules to subclass it. -
Next, you define
ColleagueWrapperas an inner class, and you declare two stored properties on it:strongColleagueandweakColleague. In some use cases, you’ll wantMediatorto retain colleagues, but in others, you won’t want this. Hence, you declare bothweakandstrongproperties to support both scenarios.Unfortunately, Swift doesn’t provide a way to limit generic type parameters to
classprotocols only. Consequently, you declarestrongColleagueandweakColleagueto be of typeAnyObject?instead ofColleagueType?. -
Next, you declare
colleagueas a computed property. This is a convenience property that first attempts to unwrapweakColleagueand, if that’snil, then it attempts to unwrapstrongColleague. -
Finally, you declare two designated initializers,
init(weakColleague:)andinit(strongColleague:), for setting eitherweakColleagueorstrongColleague.
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:
-
First, you declare
colleagueWrappersto hold onto theColleagueWrapperinstances, which will be created under the hood byMediatorfromcolleaguespassed to it. -
Next, you add a computed property for
colleagues. This usesfilterto find colleagues fromcolleagueWrappersthat have already been released and then returns an array of definitelynon-nilcolleagues. -
Finally, you declare
init(), which will act as thepublicdesignated initializer forMediator.
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:
-
As its name implies, you’ll use
addColleague(_:strongReference:)to add acolleague. Internally, this creates aColleagueWrapperthat either strongly or weakly referencescolleaguedepending on whetherstrongReferenceistrueor not. -
Likewise, you’ll use
removeColleagueto remove acolleague. In such, you first attempt to find theindexfor theColleagueWrapperthat matches thecolleagueusing pointer equality,===instead of==, so that it’s the exactColleagueTypeobject. If found, you remove the colleague wrapper at the givenindex.
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:
-
You declare
Musketeerhere, which will act as the colleague. -
You create two properties,
nameandmediator. -
Within
init, you set the properties and callmediator.addColleague(_:)to register this colleague; you’ll makeMusketeeractually conform toColleaguenext. -
Within
sendMessage, you print out thenameand passed-inmessageto the console and then callsendMessage(_:by:)on themediator. Ideally, themediatorshould 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:
-
You create
MusketeerMediatoras a subclass ofMediator<Colleague>, and you make this conform toMediatorProtocolvia an extension. -
Within
addColleague(_:), you call its super class’ method for adding a colleague,addColleague(_:strongReference:). -
Within
sendMessage(_:by:), you call its super class’ methodinvokeColleagues(by:)to send the passed-inmessageto all colleagues except for the matching passed-incolleague.
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:
-
SearchColleaguewill act as the mediator. It will conform toSearchMediatingand have strong references toSearchColleagueobjects. -
YelpSearchColleaguewill act as the colleagues. It will conform toSearchColleagueand have an unowned reference to the mediator viaSearchMediating. -
The files for
SearchColleague,SearchColleagueMediatingandYelpSearchColleaguehave 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:
-
First, you declare
SearchColleagueas a class protocol. -
Next, you define two properties:
categorywill be theYelpCategoryto search for, andselectedBusinesswill be theYLPBusinessthat has been selected.You should know that
YelpAPIactually doesn’t define categories as an enum, but rather, it defines them as strings. To ensure correct string values are used, I’ve addedYelpCategoryto Yeti Date for you with valid strings for restaurants, bars and movie theaters and corresponding icon images. -
You’ll call
update(userCoordinate:)to indicate that the user’s location has been updated. -
You’ll call
fellowColleague(_ colleague: didSelect business:)to indicate to the other colleagues that the givencolleaguehas selected a business. -
You’ll call
reset()to remove anyselectedBusiness, restore theSearchColleagueto 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:
-
You’ll call
searchColleague(_:didSelect:)whenever aSearchColleaguehas selected a business. -
You’ll call
searchColleague(_:didCreate:)to indicate that theSearchColleaguehas created new view models that need to be displayed. -
You’ll call
searchColleague(_:searchFailed:)to indicate that aSearchColleaguehas 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:
-
You declare two
publicproperties:categoryandselectedBusiness. -
You create several
privateproperties for performing searches:colleagueCoordinate,mediator,userCoordinateandyelpClient.YelpSearchColleaguewill use these to perform searches around either the user’s location, given byuserCoordinate, or around another selected colleague’s business location, given bycolleagueCoordinate. -
You declare
privateproperties for limiting search results:queryLimit, which has a default value given bydefaultQueryLimit, andquerySort, which has a default value given bydefaultQuerySort. You’ll see shortly how these are used. -
You declare the designated initializer, which accepts
categoryandmediator.
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:
-
You make
YelpSearchColleagueconform toSearchColleague, as intended per the design overview before. -
In response to receiving
fellowColleague(_:didSelect:), you set thecolleagueCoordinate, divide thequeryLimitby two, change thequerySortto.distance, and callperformSearch()to do a new search.This results in a focused search around the
colleagueCoordinate: You limit the results by reducingqueryLimitand show the closest results by changingquerySorttodistance. -
In response to receiving
update(userCoordinate:), you setself.userCoordinateand then perform a new search. -
In response to receiving
reset(), you resetcolleagueCoordinate,queryLimit,querySortandselectedBusinessto 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.
-
You first validate that
selectedBusinessisniland that there’s either a non-nilcolleagueCoordinateor a non-niluserCoordinate. If either of these isn’ttrue, you return early. -
You then set up a
YLPQueryand use this to queryYLPClient. -
If there’s not a
searchobject, then the Yelp API failed. If so, you inform themediatorand return early. -
You build up a
Set<BusinessMapViewModel>by iterating through thesearch.businesses.BusinessMapViewModelconforms toMKAnnotation, which is exactly what’s needed to be displayed on the map. -
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:
-
You make
SearchClientconform toSearchColleagueMediatingvia an extension. -
In response to
searchColleague(_:didSelect:), you do the following: (i) Notify thedelegatethat abusinesswas selected by the givencolleague; (ii) Notify the other colleagues that a business was selected; and (iii) In the event that each of thecolleagueshas aselectedBusiness, you notify thedelegatethat selection has been completed. -
In response to
searchColleague(_:didCreate:), you notify thedelegate. In turn, thedelegateis responsible for displaying these view models. -
Finally, in response to
searchColleague(_:searchFailed:), you notify thedelegate. In turn, thedelegateis 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:
-
YelpSearchClientisn’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.