🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
装饰者模式可以动态的给指定的类添加一些行为和职责,而不用对原代码进行任何修改。当你需要使用子类的时候,不妨考虑一下装饰者模式,可以在原始类上面封装一层。 在 Swift 里,有两种方式实现装饰者模式:扩展 (Extension) 和委托 (Delegation)。 ### 扩展 扩展是一种十分强大的机制,可以让你在不用继承的情况下,给已存在的类、结构体或者枚举类添加一些新的功能。最重要的一点是,你可以在你没有访问权限的情况下扩展已有类。这意味着你甚至可以扩展 Cocoa 的类,比如 `UIView` 或者 `UIImage` 。 举个例子,在编译时新加的方法可以像扩展类的正常方法一样执行。这和装饰器模式有点不同,因为扩展不会持有扩展类的对象。 ### 如何使用扩展 想象一下这个场景,我们需要在下面这个列表里展示数据: [![](http://cdn2.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern3-480x262.png)](http://cdn2.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern3-480x262.png) 专辑标题从哪里来? `Album` 本身是个 `Model` 对象,所以它不应该负责如何展示数据。你需要一些额外的代码添加展示数据的逻辑,但是为了保持 `Model` 的干净,我们不应该直接修改代码,因为这样不符合单一职责原则。 `Model` 层最好就是负责纯粹的数据结构,如果有数据的操作可以放到扩展中完成。 接下来我们会创建一个扩展,扩展现有的 `Album` 类,在扩展里定义了新的方法,返回更适合`UITableView` 展示用的数据结构。 数据的结构大概是这样: [![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/08/delegate2-480x67.png)](http://cdn5.raywenderlich.com/wp-content/uploads/2013/08/delegate2-480x67.png) 新建一个 Swift 文件:`AlbumExtensions` ,在里面添加如下扩展: ~~~ extension Album { func ae_tableRepresentation() -> (titles:[String], values:[String]) { return (["Artist", "Album", "Genre", "Year"], [artist, title, genre, year]) } } ~~~ 在方法的前面有个 `ae_` 前缀,是 `AlbumExtension` 的缩写,这样有利于和类的原有方法进行区分,避免使用的时候产生冲突。现在很多还在维护中的第三方库都已经改成了这个风格。 注意:类是可以重写父类方法的,但是在扩展里不可以。扩展里的方法和属性不能和原始类里的方法和属性冲突。 思考一下这个设计模式的强大之处: * 我们可以直接在扩展里使用 `Album` 里的属性。 * 我们给 `Album` 类添加了内容但是并没有继承它,事实上,使用继承来扩展业务也可以实现一样的功能。 * 这个简单的扩展让我们可以更好地把 `Album` 的数据展示在 `UITableView` 里,而且不用修改源码。 ### 委托 装饰者模式的另一种实现方案是委托。在这种机制下,一个对象可以和另一个对象相关联。比如你在用 `UITableView` ,你必须实现 `tableView(_:numberOfRowsInSection:)` 这个委托方法。 你不应该指望 `UITableView` 知道你有多少数据,这是个应用层该解决的问题。所以,数据相关的计算应该通过 `UITableView` 的委托来解决。这样可以让 `UITableView` 和数据层分别独立。视图层就负责显示数据,你递过来什么我就显示什么。 下面这张图很好的解释了 `UITableView` 的工作过程: [![](http://cdn5.raywenderlich.com/wp-content/uploads/2013/08/delegate-480x252.png)](http://cdn5.raywenderlich.com/wp-content/uploads/2013/08/delegate-480x252.png) `UITableView` 的工作仅仅是展示数据,但是最终它需要知道自己要展示那些数据,这时就可以向它的委托询问。在 objc 的委托模式里,一个类可以通过协议来声明可选或者必须的方法。 看起来似乎继承然后重写必须的方法来的更简单一点。但是考虑一下这个问题:继承的结果必定是一个独立的类,如果你想让某个对象成为多个对象的委托,那么子类这招就行不通了。 注意:委托模式十分重要,苹果在 UIKit 中大量使用了该模式,基本上随处可见。 ### 如何使用委托模式 打开 `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 获取所有的专辑数据,记住,我们使用外观模式之后,应该从 `LibraryAPI` 获取数据,而不是 `PersistencyManager` 。 3. 你可以在这里设置你的 `UITablweView` ,在这里声明了 `UITableView` 的 `delegate` 是当前的 `ViewController` 。事实上你用了 XIB 或者 StoryBoard ,可以直接在可视化的页面里拖拽完成。 接下来添加一个新的方法用来更方便的获取数据: ~~~ func showDataForAlbum(albumIndex: Int) { // defensive code: make sure the requested index is lower than the amount of albums if (albumIndex < allAlbums.count && albumIndex > -1) { //fetch the album let album = allAlbums[albumIndex] // save the albums data to present it later in the tableview currentAlbumData = album.ae_tableRepresentation() } else { currentAlbumData = nil } // we have the data we need, let's refresh our tableview dataTable!.reloadData() } ~~~ `showDataForAlbum()` 这个方法获取最新的专辑数据,当你想要展示新数据的时候,你需要调用`reloadData()` 这个方法,这样 `UITableView` 就会向委托请求数据,比如有多少个 `section`有多少个 `row` 之类的。 在 `viewDidLoad` 里面调用上面的方法: ~~~ self.showDataForAlbum(currentAlbumIndex) ~~~ 这样应用一启动就会去加载当前的专辑数据。因为 `currentAlbumIndex` 的默认值是 0 ,所以一开始会默认显示第一章专辑的信息。 接下来我们该去完善 `DataSource` 的协议方法了。你可以直接把委托方法写在类里面,当然如果你想让你的代码看起来更整洁一点,则可以放在扩展里。 在文件底部添加如下方法,注意一定要放在类定义的大括号外面,因为这两个家伙不是类定义的一部分,它们是扩展: ~~~ extension ViewController: UITableViewDataSource { } extension ViewController: UITableViewDelegate { } ~~~ 上面就是实现委托的方法 - 你可以把协议想象成是与委托之间的约定,只要你实现了约定的方法,就算是实现了委托。在我们的代码中, `ViewController` 需要遵守 `UITableViewDataSource`和 `UITableViewDelegate` 的协议。这样 `UITableView` 才能确保必要的委托方法都已经实现了。 在 `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:)` 返回需要展示的行数,和存储的数据中的 title 的数目相同。 `tableView(_:cellForRowAtIndexPath:)` 创建并且返回了一个单元格,上面有标题和对应的值。 注意:你可以把这些方法直接加在类声明里面,也可以放在扩展里,编译器不会去管数据源到底在哪里,只要能找到对应的方法就可以了。而我们之所以这样做,是为了方便其他人阅读。 此时再构建项目,你可以看到如下内容: [![](http://cdn3.raywenderlich.com/wp-content/uploads/2014/11/Screen-Shot-2014-11-11-at-12.38.53-AM-179x320.png)](http://cdn3.raywenderlich.com/wp-content/uploads/2014/11/Screen-Shot-2014-11-11-at-12.38.53-AM-179x320.png) 是的,显示成功啦!目前的项目源码在这里:[BlueLibrarySwift-Part1](http://cdn5.raywenderlich.com/wp-content/uploads/2014/11/BlueLibrarySwift-Part1.zip),如果遇到什么问题你可以下载下来对比一下。