裝飾者模式可以動態(tài)的給指定的類添加一些行為和職責(zé),而不用對原代碼進行任何修改。當(dāng)你需要使用子類的時候,不妨考慮一下裝飾者模式,可以在原始類上面封裝一層。
在 Swift 里,有兩種方式實現(xiàn)裝飾者模式:擴展 (Extension) 和委托 (Delegation)。
擴展是一種十分強大的機制,可以讓你在不用繼承的情況下,給已存在的類、結(jié)構(gòu)體或者枚舉類添加一些新的功能。最重要的一點是,你可以在你沒有訪問權(quán)限的情況下擴展已有類。這意味著你甚至可以擴展 Cocoa 的類,比如 UIView
或者 UIImage
。
舉個例子,在編譯時新加的方法可以像擴展類的正常方法一樣執(zhí)行。這和裝飾器模式有點不同,因為擴展不會持有擴展類的對象。
想象一下這個場景,我們需要在下面這個列表里展示數(shù)據(jù):
專輯標(biāo)題從哪里來? Album
本身是個 Model
對象,所以它不應(yīng)該負責(zé)如何展示數(shù)據(jù)。你需要一些額外的代碼添加展示數(shù)據(jù)的邏輯,但是為了保持 Model
的干凈,我們不應(yīng)該直接修改代碼,因為這樣不符合單一職責(zé)原則。 Model
層最好就是負責(zé)純粹的數(shù)據(jù)結(jié)構(gòu),如果有數(shù)據(jù)的操作可以放到擴展中完成。
接下來我們會創(chuàng)建一個擴展,擴展現(xiàn)有的 Album
類,在擴展里定義了新的方法,返回更適合 UITableView
展示用的數(shù)據(jù)結(jié)構(gòu)。
數(shù)據(jù)的結(jié)構(gòu)大概是這樣:
新建一個 Swift 文件:AlbumExtensions
,在里面添加如下擴展:
extension Album {
func ae_tableRepresentation() -> (titles:[String], values:[String]) {
return (["Artist", "Album", "Genre", "Year"], [artist, title, genre, year])
}
}
在方法的前面有個 ae_
前綴,是 AlbumExtension
的縮寫,這樣有利于和類的原有方法進行區(qū)分,避免使用的時候產(chǎn)生沖突?,F(xiàn)在很多還在維護中的第三方庫都已經(jīng)改成了這個風(fēng)格。
注意:類是可以重寫父類方法的,但是在擴展里不可以。擴展里的方法和屬性不能和原始類里的方法和屬性沖突。
思考一下這個設(shè)計模式的強大之處:
Album
里的屬性。Album
類添加了內(nèi)容但是并沒有繼承它,事實上,使用繼承來擴展業(yè)務(wù)也可以實現(xiàn)一樣的功能。Album
的數(shù)據(jù)展示在 UITableView
里,而且不用修改源碼。裝飾者模式的另一種實現(xiàn)方案是委托。在這種機制下,一個對象可以和另一個對象相關(guān)聯(lián)。比如你在用 UITableView
,你必須實現(xiàn) tableView(_:numberOfRowsInSection:)
這個委托方法。
你不應(yīng)該指望 UITableView
知道你有多少數(shù)據(jù),這是個應(yīng)用層該解決的問題。所以,數(shù)據(jù)相關(guān)的計算應(yīng)該通過 UITableView
的委托來解決。這樣可以讓 UITableView
和數(shù)據(jù)層分別獨立。視圖層就負責(zé)顯示數(shù)據(jù),你遞過來什么我就顯示什么。
下面這張圖很好的解釋了 UITableView
的工作過程:
UITableView
的工作僅僅是展示數(shù)據(jù),但是最終它需要知道自己要展示那些數(shù)據(jù),這時就可以向它的委托詢問。在 objc 的委托模式里,一個類可以通過協(xié)議來聲明可選或者必須的方法。
看起來似乎繼承然后重寫必須的方法來的更簡單一點。但是考慮一下這個問題:繼承的結(jié)果必定是一個獨立的類,如果你想讓某個對象成為多個對象的委托,那么子類這招就行不通了。
注意:委托模式十分重要,蘋果在 UIKit 中大量使用了該模式,基本上隨處可見。
打開 ViewController.swift
文件,添加如下私有變量:
private var allAlbums = [Album]()
private var currentAlbumData : (titles:[String], values:[String])?
private var currentAlbumIndex = 0
在 viewDidLoad
里面加入如下內(nèi)容:
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.translucent = false
currentAlbumIndex = 0
allAlbums = LibraryAPI.sharedInstance.getAlbums()
dataTable.delegate = self
dataTable.dataSource = self
dataTable.backgroundView = nil
view.addSubview(dataTable!)
}
對上面三個部分進行拆解:
關(guān)閉導(dǎo)航欄的透明效果
通過 API 獲取所有的專輯數(shù)據(jù),記住,我們使用外觀模式之后,應(yīng)該從 LibraryAPI
獲取數(shù)據(jù),而不是 PersistencyManager
。
UITablweView
,在這里聲明了 UITableView
的 delegate
是當(dāng)前的 ViewController
。事實上你用了 XIB 或者 StoryBoard ,可以直接在可視化的頁面里拖拽完成。接下來添加一個新的方法用來更方便的獲取數(shù)據(jù):
func showDataForAlbum(albumIndex: Int) {
if (albumIndex < allAlbums.count && albumIndex > -1) {
let album = allAlbums[albumIndex]
currentAlbumData = album.ae_tableRepresentation()
} else {
currentAlbumData = nil
}
dataTable!.reloadData()
}
showDataForAlbum()
這個方法獲取最新的專輯數(shù)據(jù),當(dāng)你想要展示新數(shù)據(jù)的時候,你需要調(diào)用 reloadData()
這個方法,這樣 UITableView
就會向委托請求數(shù)據(jù),比如有多少個 section
有多少個 row
之類的。
在 viewDidLoad
里面調(diào)用上面的方法:
self.showDataForAlbum(currentAlbumIndex)
這樣應(yīng)用一啟動就會去加載當(dāng)前的專輯數(shù)據(jù)。因為 currentAlbumIndex
的默認(rèn)值是 0 ,所以一開始會默認(rèn)顯示第一章專輯的信息。
接下來我們該去完善 DataSource
的協(xié)議方法了。你可以直接把委托方法寫在類里面,當(dāng)然如果你想讓你的代碼看起來更整潔一點,則可以放在擴展里。
在文件底部添加如下方法,注意一定要放在類定義的大括號外面,因為這兩個家伙不是類定義的一部分,它們是擴展:
extension ViewController: UITableViewDataSource {
}
extension ViewController: UITableViewDelegate {
}
上面就是實現(xiàn)委托的方法 - 你可以把協(xié)議想象成是與委托之間的約定,只要你實現(xiàn)了約定的方法,就算是實現(xiàn)了委托。在我們的代碼中, ViewController
需要遵守 UITableViewDataSource
和 UITableViewDelegate
的協(xié)議。這樣 UITableView
才能確保必要的委托方法都已經(jīng)實現(xiàn)了。
在 UITableViewDataSource
對應(yīng)的那個擴展里加上如下方法:
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:)
返回需要展示的行數(shù),和存儲的數(shù)據(jù)中的 title 的數(shù)目相同。
tableView(_:cellForRowAtIndexPath:)
創(chuàng)建并且返回了一個單元格,上面有標(biāo)題和對應(yīng)的值。
注意:你可以把這些方法直接加在類聲明里面,也可以放在擴展里,編譯器不會去管數(shù)據(jù)源到底在哪里,只要能找到對應(yīng)的方法就可以了。而我們之所以這樣做,是為了方便其他人閱讀。
此時再構(gòu)建項目,你可以看到如下內(nèi)容:
是的,顯示成功啦!目前的項目源碼在這里:BlueLibrarySwift-Part1,如果遇到什么問題你可以下載下來對比一下。
下一章我們會繼續(xù)設(shè)計模式的內(nèi)容,敬請期待!
更多建議: