中介者者模式是一种行为设计模式,封装了对象之间的通信方式。它涉及四种类型。
同事是想要相互通信的对象。它们实现了同事协议。
同事协议规定了每个同事必须实现的方法和属性。
中介者是控制同事之间通信的对象。它实现了中介者协议。
中介者协议规定了中介者必须实现的方法和属性。
每个同事都包含一个对中介者的引用,通过中介者协议。为了代替与其他同事的直接互动,每个同事都通过中介者进行交流。
中介者促进了同事之间的互动。同事们既可以发送也可以接收来自中介者的信息。
这种中介者模式对于将同事之间的互动分离到一个对象,即中介者中是很有用的。
当你需要一个或多个同事对另一个同事发起的事件采取行动,并反过来让这个同事产生影响其他同事的进一步事件时,这种模式就特别有用。
打开Starter目录下的AdvancedDesignPattern.xcworkspace,或者从你在本书中一直在继续的自己的playground工作区继续,然后从文件层次结构中打开Mediator页面。
在你写这个页面的代码例子之前,你需要创建一个基础的Mediator类。
注意:从技术上讲,你可以在不使用基中介者的情况下实现中介者模式,但如果你这样做,你可能会写出更多的模板代码。
如果你阅读了第16章 "多播代理模式",你可能会注意到Mediator类与MulticastDelegate类相似,但它有几个关键的区别,使其独一无二。
在Source下,打开Mediator.swift并添加以下代码。
// 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
}
}
}
以下是这段代码中的内容。
首先,你将Mediator定义为一个通用类,接受任何ColleagueType作为通用类型。你还声明Mediator是开放的,以使其他模块的类能够对其进行子类化。
接下来,你将ColleagueWrapper定义为一个内层类,并声明它的两个存储属性:strongColleague 和 weakColleague。在某些用例中,你希望Mediator能保留同事,但在其他情况下,你不希望这样。因此,你同时声明弱属性和强属性以支持这两种情况。
不幸的是,Swift并没有提供一种方法来限制通用类型参数,使其只适用于类协议。因此,你声明strongColleague和weakColleague的类型为AnyObject,而不是ColleagueType。
接下来,你将colleague声明为一个计算属性。这是一个方便的属性,它首先尝试解开weakColleague,如果这个属性为零,它就尝试解开strongColleague。
最后,你声明两个指定的初始化器,init(weakColleague:)和init(strongColleague:),用于设置weakColleague或strongColleague。
接下来,在ColleagueWrapper的大括号结束后添加以下代码。
依次看每个评论部分。
实例,这些实例将由Mediator从传递给它的同事中暗中创建。
接下来,你为同事添加一个计算属性。它使用过滤器从已经被释放的colleagueWrappers中找到同事,然后返回一个无限不为零的同事数组。
最后,你声明init(),它将作为Mediator的公共指定初始化器。
你还需要一种方法来添加和删除同事。在前面的代码后添加以下实例方法来实现这一点。
以下是这段代码的作用。
顾名思义,你将使用addColleague(_:strongReference:)来添加一个同事。在内部,这将创建一个ColleagueWrapper,根据strongReference是否为真,对colleague进行强或弱的引用。
同样地,你将使用removeColleague来删除一个同事。在这种情况下,你首先尝试找到与同事相匹配的ColleagueWrapper的索引,使用指针平等,===而不是===,这样它就是准确的ColleagueType对象。如果找到了,你就删除给定索引处的同事包装器。
最后,你需要一个方法来实际调用所有的同事。在removeColleague(_:)下面添加以下方法。
这两个方法都会遍历同事,也就是你之前定义的计算属性,该属性会自动剔除零实例,并在每个同事实例上调用传入的闭包。
唯一的区别是invokeColleagues(by:closure:)不会在传入的匹配同事上调用传入的闭包。这对于防止同事对自己发起的变化或事件采取行动是非常有用的。
你现在有了一个非常有用的基础中介者类,你已经准备好好好利用它了!
从文件层次结构中打开Mediator页面,在代码示例后输入这个。
你在这里声明Colleague,它要求符合要求的同事实现一个方法:colleague(_ colleague:didSendMessage:)。
接下来,在playground的末尾添加以下内容。
// MARK: - Mediator Protocol
public protocol MediatorProtocol: class {
func addColleague(_ colleague: Colleague)
func sendMessage(_ message: String, by colleague: Colleague)
}
你在这里声明MediatorProtocol,它要求符合要求的中介者实现两个方法:addColleague(:) 和sendMessage(:by:)。
正如你可能已经从这些协议中猜到的那样,你将创建一个中介者-联盟的例子,同事将通过中介者发送消息字符串。
然而,这些并不是普通的同事--那就没有任何乐趣了。相反,这些同事将是三个火枪手:传说中的剑客阿托斯、波尔朵斯和阿拉米斯,他们互相呼唤着战斗的呼声
好吧,好吧......也许这个例子有点傻,但实际上效果真的很好!而且,也许它甚至可以帮助我们了解更多。而且,也许,它甚至会帮助你记住中介者模式--"中介者设计模式就是三个火枪手互相呼叫!"
接下来输入以下代码;暂时忽略由此产生的编译器错误。
// 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)
}
}
让我们一步步来看看。
你在这里声明火枪手,它将充当同事的角色。
你创建了两个属性,名字和中介者。
在init中,你设置这些属性并调用mediator.addColleague(_:)来注册这个同事;接下来你将使Musketeer真正符合Colleague。
在sendMessage中,你打印出名字和传入的信息到控制台,然后在mediator上调用sendMessage(_:by:)。理想情况下,中介者应该把这个消息转发给所有其他的同事。
接下来,在playground的末端添加以下内容。
在这里,你让Musketeer符合Colleague的要求。为此,你实现了它的必要方法colleague(_:didSendMessage:),在那里你打印了火枪手的名字和收到的消息。
你接下来需要实现中介者。接下来添加以下代码来实现。
下面是这个的作用。
你创建MusketeerMediator作为Mediator
在addColleague(:)中,你调用其超类的方法来添加一个同事,addColleague(:strongReference:)。
在sendMessage(_:by:)中,你调用其超类的方法invokeColleagues(by:),将传入的消息发送给所有同事,除了匹配传入的同事。
这就搞定了所需的中介者类,所以你现在可以试用它们了 接下来添加以下代码。
// 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")
通过上述,你声明了一个名为mediator的MusketeerMediator实例和三个名为athos、porthos和aramis的Musketeer实例。
接下来添加以下代码来发送一些信息。
athos.sendMessage("One for all...!")
print("")
porthos.sendMessage("and all for one...!")
print("")
aramis.sendMessage("Unus pro omnibus, omnes pro uno!")
print("")
结果是,你应该看到以下内容被打印到控制台。
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!
请注意,信息发送者并没有收到信息。例如,阿托斯发送的信息被波尔朵斯和阿拉米斯收到,然而阿托斯并没有收到它。这正是你所期望发生的行为!
直接使用mediator,也可以向所有同事发送一条信息。在playground的末尾添加以下代码来实现。
mediator.invokeColleagues() {
$0.colleague(nil, didSendMessage: "Charge!")
}
这导致以下内容被打印到控制台。
Athos received: Charge!
Porthos received: Charge!
Aramis received: Charge!
这次所有的人都得到了这个消息。现在,让我们继续冲刺这个项目吧!
这种模式在解耦同事方面非常有用。每个同事不是直接互动,而是通过中介者进行交流。
然而,你需要小心把中介者变成一个 "神 "的对象--一个知道系统内所有其他对象的对象。
如果你的中介者变得太大,可以考虑将其分解为多个mediatorcolleague系统。另外,也可以考虑其他模式来分解中介者,比如委托它的一些功能。
在本章中,你将为一个名为YetiDate的应用程序添加功能。这个应用程序将帮助用户计划一个涉及三个不同地点的约会:酒吧、餐厅和电影院。它使用CocoaPods拉入YelpAPI,这是一个帮助库,用于搜索Yelp的上述地点。
在Starter目录中,在Xcode中打开YetiDate▸ YetiDate.xcworkspace(不是.xcodeproj)。
如果你以前没有使用过CocoaPods,那也没关系!你所需要的一切都已经为你准备好了。你所需要的一切已经包含在启动项目中,所以你不需要运行pod安装。你唯一需要记住的是打开YetiDate.xcworkspace,而不是YetiDate.xcodeproj文件。
在运行该应用程序之前,你首先需要注册一个Yelp API密钥。
如果你在中级阶段学习了CoffeeQuest,你已经创建了一个Yelp API密钥。你会在第10章 "模型-视图-视图模型模式 "中完成这个工作。复制你现有的密钥并将其粘贴到APIKeys.swift中的指定位置,然后跳过本节的其余部分,进入 "创建所需协议 "部分。
如果你没有通过CoffeeQuest工作,请按照这些说明来生成Yelp API密钥。
在你的网络浏览器中导航到这个URL。
如果你没有账户,就创建一个账户,或者登录。接下来,在 "创建应用程序 "表格中输入以下内容(如果你以前创建过一个应用程序,则使用你现有的API密钥)。
应用程序名称。输入 "Yeti Date"。
应用程序网站。此处留空
行业。选择 "商业"
公司。留白
联系电子邮件。输入你的电子邮件地址
描述。输入 "商业搜索应用程序"
我已阅读并接受Yelp API条款。勾选此项
你的表格应该如下所示。
按Create New App继续,你应该看到一个成功信息。
复制你的API密钥并返回到Xcode中的YetiDate.xcworkspace。
从文件层次结构中打开APIKeys.swift,将你的API密钥粘贴在指定位置。
由于该应用程序显示附近的餐馆、酒吧和电影院,它在附近有许多企业的地区效果最好。所以该应用程序的默认位置已被设置为加利福尼亚州旧金山。
注意:你可以通过点击调试▸位置,然后选择不同的选项来改变模拟器的位置。
如果你建立并运行该应用程序,你会被提示授予访问用户位置的权限。然而之后,你会看到一张空白的地图,什么也没有发生
打开PlanDateViewController.swift,它是显示这张地图的视图控制器,符合MKMapViewDelegate以接收地图相关的事件。向下滚动到mapView(_:didUpdate:),你会发现这个调用。
searchClient.update(userCoordinate: userLocation.coordinate)
这就是启动搜索附近企业的过程。打开SearchClient.swift,你会看到有几个方法里面有//TODO注释。
这里有一个关于中介者-联盟系统如何工作的概述。
SearchColleague将充当中介者。它将符合 SearchMediating,并对 SearchColleague 对象有很强的引用。
YelpSearchColleague 将充当同事。它将符合 SearchColleague 并通过 SearchMediating 对中介者有一个无主的引用。
SearchColleague、SearchColleagueMediating 和 YelpSearchColleague 的文件已经为你添加,但这些文件目前是空白的。实现它们是你的工作!
首先,打开SearchColleague.swift并添加以下内容。
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()
}
下面是这是什么,一步一步来。
首先,你声明SearchColleague是一个类协议。
2.接下来,你定义两个属性:category将是要搜索的YelpCategory,selectedBusiness将是被选中的YLPBusiness。
你应该知道,YelpAPI实际上并没有把类别定义为一个枚举,而是把它们定义为字符串。为了确保使用正确的字符串值,我已经为你在Yeti Date中添加了YelpCategory,其中包括餐馆、酒吧和电影院的有效字符串以及相应的图标图像。
你将调用update(userCoordinate:)来表示用户的位置已被更新。
你将调用fellowColleague(_ colleague: didSelect business:)来向其他同事表明,给定的同事已经选择了一项业务。
你将调用reset()来删除任何selectedBusiness,将SearchColleague恢复到其初始搜索状态,并执行新的搜索。打开SearchColleagueMediating.swift并添加以下内容。
以下是你将如何使用这些方法。
每当一个SearchColleague选择了一个企业,你将调用searchColleague(_:didSelect:)。
你将调用searchColleague(_:didCreate:)来表示SearchColleague已经创建了需要显示的新视图模型。
你将调用 searchColleague(_:searchFailed:) 来表示 SearchColleague 在搜索时遇到了网络错误。打开 YelpSearchColleague.swift 并添加这个。
以下是你所做的事情。
你声明了两个公共属性:类别和选定的业务。
你创建了几个用于执行搜索的私有属性。colleagueCoordinate, mediator, userCoordinate 和 yelpClient。YelpSearchColleague 将使用这些属性围绕用户的位置(由 userCoordinate 提供)或围绕另一个选定的同事的业务位置(由 colleagueCoordinate 提供)进行搜索。
你声明了用于限制搜索结果的私有属性:queryLimit,它的默认值由defaultQueryLimit给出;querySort,它的默认值由defaultQuerySort给出。你很快就会看到这些是如何使用的。
你声明指定的初始化器,它接受类别和中介者。
接下来,在文件的末尾添加以下内容。
我们来看看这个。
你使 YelpSearchColleague 符合 SearchColleague,就像之前的设计概述中所说的那样。
作为对收到 fellowColleague(_:didSelect:) 的回应,你设置 colleagueCoordinate,将 queryLimit 除以 2,将 querySort 改为 .distance,并调用 performSearch() 来进行新的搜索。
这将产生一个围绕 colleagueCoordinate 的集中搜索。你通过减少 queryLimit 来限制结果,并通过改变 querySort 为 distance 来显示最近的结果。
作为对收到update(userCoordinate:)的回应,你设置self.userCoordinate,然后执行新的搜索。
响应接收到的 reset(),你将 colleagueCoordinate, queryLimit, querySort 和 selectedBusiness 重置为它们的默认值,然后执行一个新的搜索。
接下来,将 performSearch() 的内容替换为以下内容。
这似乎是一个很大的工作,但实际上它并不难理解。
首先验证selectedBusiness为零,并且有一个非零的colleagueCoordinate或一个非零的userCoordinate。如果其中任何一个不是真的,你就提前返回。
然后,你设置一个YLPQuery,用它来查询YLPClient。
如果没有一个搜索对象,那么Yelp API就失败了。如果是这样,你就通知中介者并提前返回。
你通过迭代search.business来建立一个Set
你调度到主队列并通知中介者,视图模型是由YelpSearchColleague创建的。
太好了! 这就解决了同事的问题,你现在可以完成中介者的实现了。
打开 SearchClient.swift 并将类的声明替换为以下内容。
public class SearchClient: Mediator<SearchColleague> {
这里,你让 SearchClient 成为 Mediator
在文件的末尾添加以下代码。
下面是这个的作用。
你通过一个扩展使SearchClient符合SearchColleagueMediating。
为了响应searchColleague(_:didSelect:),你做以下工作。(i) 通知委托人,指定的同事选择了一项业务;(ii) 通知其他同事,选择了一项业务;以及(iii) 如果每个同事都有一项选定的业务,你通知委托人,选择已经完成。
作为对searchColleague(_:didCreate:)的响应,你通知委托人。反过来,该委托人负责显示这些视图模型。
最后,在响应searchColleague(_:searchFailed:)时,你通知委托人。反过来,该委托人负责处理错误和/或重试。
只剩下几个方法了! 将setupColleagues()的内容替换为以下内容。
通过这段代码,你为.restaurant、.bar和.movieTheaters类别创建YelpSearchColleagues。
将update(userCoordinate:)的内容替换为以下内容。
invokeColleagues() { colleague in
colleague.update(userCoordinate: userCoordinate)
}
作为对获得新的userCoordinate的回应,你把它传递给每个SearchColleague实例。
最后,将reset()的内容替换为以下内容。
invokeColleagues() {
colleague in colleague.reset()
}
同样地,你只需将reset()调用传递给每个SearchColleague实例。
喔,那是一个很大的工作! 干得好!
建立并运行该应用程序。现在地图应该显示餐厅、酒吧和电影院。
点击一个图标,你会看到一个带有绿色复选标记的呼出。
点击复选标记后,相关的YelpSearchColleague将获得它的selectedBusiness设置,将此传达给它的中介者,触发其他同事做一个新的搜索,并最终生成新的视图模型以显示在地图上 最终,一旦你选择了每个业务类型中的一个,你会看到一个屏幕显示你的选择。
你在本章中了解了中介者模式。下面是它的关键点。
中介者模式封装了对象之间的通信方式。它涉及四种类型:同事、同事协议、中介者和中介者协议。
同事是进行通信的对象;同事协议规定了所有同事必须拥有的方法和属性;中介者控制同事的通信;中介者协议规定了中介者必须拥有的必要方法和属性。
为了代替直接交谈,同事们抓住并通过中介者进行交流。同事协议和中介者协议有助于防止所有参与对象之间的紧密耦合。
在本章中,你还创建了Yeti Dates! 这是一个整洁的应用程序,但你还可以用它做很多事情。
YelpSearchClient 的搜索效率并不高。你可以通过使用缓存和只在绝对需要的时候进行搜索来改善这一点。
在为每个 YelpSearchClient 选择企业后,会出现一个 "评论日期 "页面,但这是非常基本的。你可以做很多事情来改善这一点,比如提供导航到每个地址的选项。
为什么只停留在餐馆、酒吧和电影院?你可以让用户挑选他们感兴趣的任何一个类别来分组。
这些都可以使用你在本书中学到的现有模式。你可以随心所欲地继续构建Yeti Date。
当你准备好了,就继续下一章,学习组合设计模式。
上一章 | 目录 | 下一章 |
---|