Swift中的iOS设计模式(二)

Facade设计模式

pic

Facade设计模式为多个子模块或子系统提供统一的、单独的API接口。也就是说,不用给用户暴露一堆乱七八糟的接口,只需要暴露一个简单的、标准的接口即可。

下面这张图能更好的描述这个概念:

pic

用户在使用我们暴露的标准的API时,根本不知道在这个API底下其实疏导着大量复杂的接口。该设计模式是暴露大量接口的最佳模式,尤其当接口使用很复杂、很难理解时,尤为体现该模式的价值。

Facade模式可以有效保证调用各个模块功能的接口和你隐藏起来的实现模块功能的逻辑代码是一个松耦合的关系。同时也可以减少你的子系统或子模块对外部代码的依赖。例如一个遵循Facade模式的类,如果逻辑有变化,需要修改内部实现代码,但是并不需要修改该逻辑的接口。使用接口的用户甚至根本都不知道你已经修改了该接口背后的某些逻辑功能。

如何使用Facade设计模式

目前,你们已经有PersistencyManager类用于在本地保存专辑的数据,以及HTTPClient类用于和远程服务器进行交互。项目中的其他类压根不会知道这两个类中具体的处理逻辑,因为他们将要遵循Facade模式,隐藏与LibraryAPI身后。

要遵循Facade模式,我们要有一个暴露给用户接口的类,那就是LibraryAPI,它持有PersistencyManagerHTTPClient的实例,并暴露一些简单的接口来访问这两个类中的方法:

pic

LibraryAPI就是作为暴露给其他类的标准接口,它能有效的向接口使用者规避掉HTTPClientPersistencyManager复杂的逻辑代码。

打开LibraryAPI.swift文件添加如下常量属性:


private let persistencyManager: PersistencyManager
private let httpClient: HTTPClient
private let isOnline: Bool

isOnline属性决定了是否向远程服务器上更新专辑数据的变化,比如添加或删除专辑。

然后需要添加init初始化方法:


override init() {
persistencyManager = PersistencyManager()
httpClient = HTTPClient()
isOnline = false

super.init()
}

因为本篇教程中的示例应用只是为了向大家介绍如何运用各种设计模式,所以没有远程服务器的需求,HTTPClient自然不会用到。所以这里isOnline属性总是设置为false

下一步,向LibraryAPI.swift文件中添加如下三个方法:


func getAlbums() -> [Album] {
return persistencyManager.getAlbums()
}

func addAlbum(album: Album, index: Int) {
persistencyManager.addAlbum(album, index: index)
if isOnline {
httpClient.postRequest("/api/addAlbum", body: album.description())
}
}

func deleteAlbum(index: Int) {
persistencyManager.deleteAlbumAtIndex(index)
if isOnline {
httpClient.postRequest("/api/deleteAlbum", body: "\(index)")
}
}

大家看一下addAlbum(_:index:)方法,该方法顾名思义是添加专辑的方法,在实现时会先更新本地的数据,如果网络连通且需要使用远程服务的时候,再调用远程服务接口更新数据状态。当该模块以外的类调用了LibraryAPI接口的addAlbum方法时,它并不知道添加专辑的具体实现逻辑,并且也不需要知道,这就是Facade设计模式的魅力所在。

现在编译运行你们的应用。你们会看到两个空的视图,和一个Toolbar。顶部的视图用于显示专辑的封面,下面的视图会以列表的形式显示该专辑的相关信息。

pic

接下来的工作就是将专辑的数据或图片等显示在屏幕上,这就给我们带来了学习另一个设计模式的机会 – Decorator设计模式

Decorator设计模式

Decorator模式可以自动的为对象添加某些行为或响应能力,并且不需要对该对象做任何修改。

该模式可以通过将希望添加的行为或响应能力打包到另一个对象中,然后通过该对象获得添加的行为或响应能力。

在Swift中,有两种最为常用的实现该模式的方案: ExtensionsDelegation

Extensions

你们可以对class、struct或者enum添加Extension,用于添加新的行为或响应能力,最关键的是不需要让它们继承乱七八糟的父类。更厉害的是通过Extension你们甚至不需要去访问目标class、struct或enum,就可以给它们添加新的能力。这意味着,你们可以让Cocoa框架中的对象元素更加符合你们自己的口味,比如给UIView或UIImage添加你们想要的能力。这就是Extension机制的强大之处。

这里要注意,通过Extension新添加的方法在编译阶段被加载,然后你们可以像使用某类的原生方法一样使用扩展的方法。但是与传统的Decorator模式有些许不同的是,扩展体不会持有被扩展对象的实例,说白了就是你不能实例化一个扩展体,只能通过实例化被扩展的对象,才能使用其扩展的方法。

如何使用Extensions

下图中展示了一种情况,你现在有了Albun对象,然后你想把该对象中的数据展示在一个UITableView中:

pic

在该情况下,专辑的这些数据从哪来呢?很明显是从Album对象中获取,因为它是一个Model对象,而且它根本不关心它持有的数据要如何展现,展现在哪里。那么这时,你就要使用额外的代码让Album具备将数据按需归位的能力,并且不能直接修改该类。

这就需要使用Extension机制了,你们将通过Extension机制扩展Album类,目的是给Album添加一个方法,并返回一个UITableView便于使用的数据结构。

该数据结构图如下:

pic

接下来我们新建一个Swift文件,名为 AlbumExtensions ,然后打开 AlbumExtensions.swift 文件,添加如下代码:


extension Album {
func ae_tableRepresentation() -> (titles:[String], values:[String]) {
return (["Artist", "Album", "Genre", "Year"], [artist, title, genre, year])
}
}

这里注意该方法名的开头ae_,它是AlbumExtensions的缩写。这是一个约定俗成的扩展方法名的写法,目的在于防止扩展方法与原生方法名产生冲突。

注意:通常类可以重写父类的方法或属性,但是在Extension中不可以。Extension中的方法名、属性名也不能和原生方法名、原生属性名相同。

我们再来总结一下Extension机制:

  • Album被扩展后,直接使用Album访问扩展方法。
  • 你们通过扩展的方式已经给Album添加了新的能力,如果你们还想通过子类的方式,那么依然可以这么做。
  • 上面这个简单的扩展可以看出,你们不用对Album做任何改动,它就具备了可以返回适用于UITableView数据格式的能力。

Delegation

另一个Decorator设计模式是Delegation,它是一种代表其他对象或协调其他对象的机制。

比如当你使用UITableView时,你必须要实现tableView(_:numberOfRowsInSection:)方法,你不要期望UITableView会知道你希望每个Section里有多少行。因此,计算每个Section有多少行的任务就交给了UITableView的代理。这样就可以使UITableView显示与数据松耦合,独立开来。

下图是UITableView和它的代理之间的运行关系示意图:

pic

UITableView的工作是将一些信息展示在一个列表视图中。但是它只关注于展示,并不会持有需要展示的数据。那么这时就需要向它的代理询问获取相关的信息了。在Objective-C的代理模式中,代理或协议可以申明两种类型的方法,一种是必须类型,另一种是可选类型。前者是遵循该协议的类必须要实现的方法,后者是可以实现,也可以不实现的方法。你们在该教程中会实践到这些内容。

这时大伙可能会有疑问了,为何我们要使用协议而不直接继承一个对象,然后重写需要的方法呢?这样不是更省事么?但是你们考虑一下,如果这样做,你们只能基于一个单独的父类去实现其子类,也就是说,该父类只能作为某一个对象的代理。如果想让一个对象成为多个对象的代理,那么用继承父类这种形式就行不通了。

注意:代理机制是一个很重要的模式。Apple在UIKit框架中大量应用了该模式,比如UITableViewUITextViewUITextFieldUIWebViewUIAlertUIActionSheetUICollectionViewUIPickerViewUIGestrueRecognizerUIScrollView等等。

如何使用Delegate模式

打开ViewController.swift文件,然后添加以下私有属性:


private var allAlbums = [Album]()
private var currentAlbumData : (titles:[String], values:[String])?
private var currentAlbumIndex = 0

然后重写viewDidLoad方法:


override func viewDidLoad() {
super.viewDidLoad()
//1
self.navigationController?.navigationBar.translucent = false
currentAlbumIndex = 0

//2
allAlbums = LibraryAPI.sharedInstance.getAlbums()

// 3
// the uitableview that presents the album data
dataTable.delegate = self
dataTable.dataSource = self
dataTable.backgroundView = nil
view.addSubview(dataTable!)
}

我们对上述的代码来进行一一讲解:

  1. 关闭导航栏的透明效果。
  2. 通过API获取所有专辑的列表。这里要记住,要使用符合Facade模式的LibraryAPI而不是直接使用PersisencyManager
  3. UITableView进行相关设置,将它的delegate和datasource设置为当前的ViewController。这样一来UITableView所有对数据的请求都会由当前的ViewController响应并提供了。这里需要注意的是如果你们在Storeboard中创建UIViewControllerUITableView,那么也可以在Storeboard中通过拖拽来设置delegate和datasource。

接下来,在ViewController.swift中添加如下方法:


func showDataForAlbum(albumIndex: Int) {
// 保证代码健壮性,确保专辑数至少大于0,避免数组下标越界的错误
if (albumIndex < allAlbums.count && albumIndex > -1) {
//获取专辑对象
let album = allAlbums[albumIndex]
// 保存专辑的数据,用于一会在tableview中展现
currentAlbumData = album.ae_tableRepresentation()
} else {
currentAlbumData = nil
}
// 我们已经获取到了需要的数据,可以刷新tableview来显示数据了
dataTable!.reloadData()
}

showDataForAlbum()方法的作用是从专辑数组中获取到专辑的对象,然后请求并保存专辑的数据。当你想显示新的数据或有改变的数据时,你只需要调用reloadData方法即可。这样UItableView会重新请求它的代理获取相关数据,比如一共要显示多少个Section、每个Section里显示多少行、每行看起来是什么样的等等。

我们在viewDidLoad方法中再加入一行代码:


self.showDataForAlbum(currentAlbumIndex)

该行代码让应用启动时就开始加载专辑数据。因为之前已经将currentAlbumIndex的值设为了0,所以从专辑数组中的第一个专辑开始显示。

现在,是时候实现DataSource协议的方法了,你们可以将DataSource的方法直接写在ViewController里。也可以通过使用扩展的方式使代码保持整洁。

如果使用扩展的方式,那么一定要确保它们是写在文件的最下面,并且要在ViewController类定义的大括号之外!


extension ViewController: UITableViewDataSource {
}

extension ViewController: UITableViewDelegate {
}

上面这两行代码的含义就是ViewController通过扩展的方式遵循了Delegate和DataSource协议 – 你可以把协议想象成是与委托之间的约定,只要你实现了约定的方法,就算是实现了委托。在我们的代码中,ViewController需要遵守UITableViewDataSourceUITableViewDelegate这两个协议。这样 UITableView 才能明确的知道,需要用到的代理中的方法是由这个ViewController实现的。

在遵循UITableViewDataSource协议的扩展中添加如下代码:


func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let albumData = currentAlbumData {
return albumData.titles.count
} else {
return 0
}
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
if let albumData = currentAlbumData {
cell.textLabel?.text = albumData.titles[indexPath.row]
if let detailTextLabel = cell.detailTextLabel {
detailTextLabel.text = albumData.values[indexPath.row]
}
}
return cell
}

tableView(_:numberOfRowsInSection:)方法返回每个Section中显示多少行内容,这里对应着专辑数据结构中的标题。

tableView(_:cellForRowAtIndexPath:)方法会逐个创建每行的内容,包括专辑标题和它的值。

注意:你们可以把这些协议的方法直接加在类声明里面,也可以放在扩展里,编译器不会去管DataSource的方法是放在扩展里还是类申明里,只要实现了必须的方法即可。而我们之所以这样写,是为了保证代码的整洁性和可读性。

编译并运行你们的项目,你们的应用应该已经可以显示出专辑的基本信息了:

pic

未完待续……

原文地址:Introducing iOS Design Patterns in Swift

分享到: