第19章:中介者模式

中介者者模式是一种行为设计模式,封装了对象之间的通信方式。它涉及四种类型。

  1. 同事是想要相互通信的对象。它们实现了同事协议。

  2. 同事协议规定了每个同事必须实现的方法和属性。

  3. 中介者是控制同事之间通信的对象。它实现了中介者协议。

  4. 中介者协议规定了中介者必须实现的方法和属性。

每个同事都包含一个对中介者的引用,通过中介者协议。为了代替与其他同事的直接互动,每个同事都通过中介者进行交流。

中介者促进了同事之间的互动。同事们既可以发送也可以接收来自中介者的信息。

你什么时候应该使用它?

这种中介者模式对于将同事之间的互动分离到一个对象,即中介者中是很有用的。

当你需要一个或多个同事对另一个同事发起的事件采取行动,并反过来让这个同事产生影响其他同事的进一步事件时,这种模式就特别有用。

playground示例

打开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 
    }
  }
}

以下是这段代码中的内容。

  1. 首先,你将Mediator定义为一个通用类,接受任何ColleagueType作为通用类型。你还声明Mediator是开放的,以使其他模块的类能够对其进行子类化。

  2. 接下来,你将ColleagueWrapper定义为一个内层类,并声明它的两个存储属性:strongColleague 和 weakColleague。在某些用例中,你希望Mediator能保留同事,但在其他情况下,你不希望这样。因此,你同时声明弱属性和强属性以支持这两种情况。

    不幸的是,Swift并没有提供一种方法来限制通用类型参数,使其只适用于类协议。因此,你声明strongColleague和weakColleague的类型为AnyObject,而不是ColleagueType。

  3. 接下来,你将colleague声明为一个计算属性。这是一个方便的属性,它首先尝试解开weakColleague,如果这个属性为零,它就尝试解开strongColleague。

  4. 最后,你声明两个指定的初始化器,init(weakColleague:)和init(strongColleague:),用于设置weakColleague或strongColleague。

接下来,在ColleagueWrapper的大括号结束后添加以下代码。

依次看每个评论部分。

  1. 首先,你声明colleagueWrappers,以保持ColleagueWrapper的存在。

实例,这些实例将由Mediator从传递给它的同事中暗中创建。

  1. 接下来,你为同事添加一个计算属性。它使用过滤器从已经被释放的colleagueWrappers中找到同事,然后返回一个无限不为零的同事数组。

  2. 最后,你声明init(),它将作为Mediator的公共指定初始化器。

你还需要一种方法来添加和删除同事。在前面的代码后添加以下实例方法来实现这一点。

以下是这段代码的作用。

  1. 顾名思义,你将使用addColleague(_:strongReference:)来添加一个同事。在内部,这将创建一个ColleagueWrapper,根据strongReference是否为真,对colleague进行强或弱的引用。

  2. 同样地,你将使用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) 
 }
}

让我们一步步来看看。

  1. 你在这里声明火枪手,它将充当同事的角色。

  2. 你创建了两个属性,名字和中介者。

  3. 在init中,你设置这些属性并调用mediator.addColleague(_:)来注册这个同事;接下来你将使Musketeer真正符合Colleague。

  4. 在sendMessage中,你打印出名字和传入的信息到控制台,然后在mediator上调用sendMessage(_:by:)。理想情况下,中介者应该把这个消息转发给所有其他的同事。

接下来,在playground的末端添加以下内容。

在这里,你让Musketeer符合Colleague的要求。为此,你实现了它的必要方法colleague(_:didSendMessage:),在那里你打印了火枪手的名字和收到的消息。

你接下来需要实现中介者。接下来添加以下代码来实现。

下面是这个的作用。

  1. 你创建MusketeerMediator作为Mediator的子类,并通过一个扩展使其符合MediatorProtocol。

  2. 在addColleague(:)中,你调用其超类的方法来添加一个同事,addColleague(:strongReference:)。

  3. 在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密钥。

注册Yelp的API密钥

如果你在中级阶段学习了CoffeeQuest,你已经创建了一个Yelp API密钥。你会在第10章 "模型-视图-视图模型模式 "中完成这个工作。复制你现有的密钥并将其粘贴到APIKeys.swift中的指定位置,然后跳过本节的其余部分,进入 "创建所需协议 "部分。

如果你没有通过CoffeeQuest工作,请按照这些说明来生成Yelp API密钥。

在你的网络浏览器中导航到这个URL。

如果你没有账户,就创建一个账户,或者登录。接下来,在 "创建应用程序 "表格中输入以下内容(如果你以前创建过一个应用程序,则使用你现有的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.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()
}

下面是这是什么,一步一步来。

  1. 首先,你声明SearchColleague是一个类协议。

  2. 2.接下来,你定义两个属性:category将是要搜索的YelpCategory,selectedBusiness将是被选中的YLPBusiness。

你应该知道,YelpAPI实际上并没有把类别定义为一个枚举,而是把它们定义为字符串。为了确保使用正确的字符串值,我已经为你在Yeti Date中添加了YelpCategory,其中包括餐馆、酒吧和电影院的有效字符串以及相应的图标图像。

  1. 你将调用update(userCoordinate:)来表示用户的位置已被更新。

  2. 你将调用fellowColleague(_ colleague: didSelect business:)来向其他同事表明,给定的同事已经选择了一项业务。

  3. 你将调用reset()来删除任何selectedBusiness,将SearchColleague恢复到其初始搜索状态,并执行新的搜索。打开SearchColleagueMediating.swift并添加以下内容。

以下是你将如何使用这些方法。

  1. 每当一个SearchColleague选择了一个企业,你将调用searchColleague(_:didSelect:)。

  2. 你将调用searchColleague(_:didCreate:)来表示SearchColleague已经创建了需要显示的新视图模型。

  3. 你将调用 searchColleague(_:searchFailed:) 来表示 SearchColleague 在搜索时遇到了网络错误。打开 YelpSearchColleague.swift 并添加这个。

以下是你所做的事情。

  1. 你声明了两个公共属性:类别和选定的业务。

  2. 你创建了几个用于执行搜索的私有属性。colleagueCoordinate, mediator, userCoordinate 和 yelpClient。YelpSearchColleague 将使用这些属性围绕用户的位置(由 userCoordinate 提供)或围绕另一个选定的同事的业务位置(由 colleagueCoordinate 提供)进行搜索。

  3. 你声明了用于限制搜索结果的私有属性:queryLimit,它的默认值由defaultQueryLimit给出;querySort,它的默认值由defaultQuerySort给出。你很快就会看到这些是如何使用的。

  4. 你声明指定的初始化器,它接受类别和中介者。

接下来,在文件的末尾添加以下内容。

我们来看看这个。

  1. 你使 YelpSearchColleague 符合 SearchColleague,就像之前的设计概述中所说的那样。

  2. 作为对收到 fellowColleague(_:didSelect:) 的回应,你设置 colleagueCoordinate,将 queryLimit 除以 2,将 querySort 改为 .distance,并调用 performSearch() 来进行新的搜索。

    这将产生一个围绕 colleagueCoordinate 的集中搜索。你通过减少 queryLimit 来限制结果,并通过改变 querySort 为 distance 来显示最近的结果。

  3. 作为对收到update(userCoordinate:)的回应,你设置self.userCoordinate,然后执行新的搜索。

  4. 响应接收到的 reset(),你将 colleagueCoordinate, queryLimit, querySort 和 selectedBusiness 重置为它们的默认值,然后执行一个新的搜索。

接下来,将 performSearch() 的内容替换为以下内容。

这似乎是一个很大的工作,但实际上它并不难理解。

  1. 首先验证selectedBusiness为零,并且有一个非零的colleagueCoordinate或一个非零的userCoordinate。如果其中任何一个不是真的,你就提前返回。

  2. 然后,你设置一个YLPQuery,用它来查询YLPClient。

  3. 如果没有一个搜索对象,那么Yelp API就失败了。如果是这样,你就通知中介者并提前返回。

  4. 你通过迭代search.business来建立一个Set。BusinessMapViewModel符合MKAnnotation,这正是需要在地图上显示的内容。

  5. 你调度到主队列并通知中介者,视图模型是由YelpSearchColleague创建的。

太好了! 这就解决了同事的问题,你现在可以完成中介者的实现了。

打开 SearchClient.swift 并将类的声明替换为以下内容。

public class SearchClient: Mediator<SearchColleague> {

这里,你让 SearchClient 成为 Mediator 的子类,而不是 NSObject。

在文件的末尾添加以下代码。

下面是这个的作用。

  1. 你通过一个扩展使SearchClient符合SearchColleagueMediating。

  2. 为了响应searchColleague(_:didSelect:),你做以下工作。(i) 通知委托人,指定的同事选择了一项业务;(ii) 通知其他同事,选择了一项业务;以及(iii) 如果每个同事都有一项选定的业务,你通知委托人,选择已经完成。

  3. 作为对searchColleague(_:didCreate:)的响应,你通知委托人。反过来,该委托人负责显示这些视图模型。

  4. 最后,在响应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! 这是一个整洁的应用程序,但你还可以用它做很多事情。

这些都可以使用你在本书中学到的现有模式。你可以随心所欲地继续构建Yeti Date。

当你准备好了,就继续下一章,学习组合设计模式。


上一章 目录 下一章