裝飾者模式

2023-05-10 10:41 更新

裝飾者模式 - Decorator

裝飾者模式可以動(dòng)態(tài)的給指定的類(lèi)添加一些行為和職責(zé),而不用對(duì)原代碼進(jìn)行任何修改。當(dāng)你需要使用子類(lèi)的時(shí)候,不妨考慮一下裝飾者模式,可以在原始類(lèi)上面封裝一層。

在 Swift 里,有兩種方式實(shí)現(xiàn)裝飾者模式:擴(kuò)展 (Extension) 和委托 (Delegation)。

擴(kuò)展

擴(kuò)展是一種十分強(qiáng)大的機(jī)制,可以讓你在不用繼承的情況下,給已存在的類(lèi)、結(jié)構(gòu)體或者枚舉類(lèi)添加一些新的功能。最重要的一點(diǎn)是,你可以在你沒(méi)有訪問(wèn)權(quán)限的情況下擴(kuò)展已有類(lèi)。這意味著你甚至可以擴(kuò)展 Cocoa 的類(lèi),比如 UIView 或者 UIImage 。

舉個(gè)例子,在編譯時(shí)新加的方法可以像擴(kuò)展類(lèi)的正常方法一樣執(zhí)行。這和裝飾器模式有點(diǎn)不同,因?yàn)閿U(kuò)展不會(huì)持有擴(kuò)展類(lèi)的對(duì)象。

如何使用擴(kuò)展

想象一下這個(gè)場(chǎng)景,我們需要在下面這個(gè)列表里展示數(shù)據(jù):

專(zhuān)輯標(biāo)題從哪里來(lái)? Album 本身是個(gè) Model 對(duì)象,所以它不應(yīng)該負(fù)責(zé)如何展示數(shù)據(jù)。你需要一些額外的代碼添加展示數(shù)據(jù)的邏輯,但是為了保持 Model 的干凈,我們不應(yīng)該直接修改代碼,因?yàn)檫@樣不符合單一職責(zé)原則。 Model 層最好就是負(fù)責(zé)純粹的數(shù)據(jù)結(jié)構(gòu),如果有數(shù)據(jù)的操作可以放到擴(kuò)展中完成。

接下來(lái)我們會(huì)創(chuàng)建一個(gè)擴(kuò)展,擴(kuò)展現(xiàn)有的 Album 類(lèi),在擴(kuò)展里定義了新的方法,返回更適合 UITableView 展示用的數(shù)據(jù)結(jié)構(gòu)。

數(shù)據(jù)的結(jié)構(gòu)大概是這樣:

新建一個(gè) Swift 文件:AlbumExtensions ,在里面添加如下擴(kuò)展:

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

在方法的前面有個(gè) ae_ 前綴,是 AlbumExtension 的縮寫(xiě),這樣有利于和類(lèi)的原有方法進(jìn)行區(qū)分,避免使用的時(shí)候產(chǎn)生沖突?,F(xiàn)在很多還在維護(hù)中的第三方庫(kù)都已經(jīng)改成了這個(gè)風(fēng)格。

注意:類(lèi)是可以重寫(xiě)父類(lèi)方法的,但是在擴(kuò)展里不可以。擴(kuò)展里的方法和屬性不能和原始類(lèi)里的方法和屬性沖突。

思考一下這個(gè)設(shè)計(jì)模式的強(qiáng)大之處:

  • 我們可以直接在擴(kuò)展里使用 Album 里的屬性。
  • 我們給 Album 類(lèi)添加了內(nèi)容但是并沒(méi)有繼承它,事實(shí)上,使用繼承來(lái)擴(kuò)展業(yè)務(wù)也可以實(shí)現(xiàn)一樣的功能。
  • 這個(gè)簡(jiǎn)單的擴(kuò)展讓我們可以更好地把 Album 的數(shù)據(jù)展示在 UITableView 里,而且不用修改源碼。

委托

裝飾者模式的另一種實(shí)現(xiàn)方案是委托。在這種機(jī)制下,一個(gè)對(duì)象可以和另一個(gè)對(duì)象相關(guān)聯(lián)。比如你在用 UITableView ,你必須實(shí)現(xiàn) tableView(_:numberOfRowsInSection:) 這個(gè)委托方法。

你不應(yīng)該指望 UITableView 知道你有多少數(shù)據(jù),這是個(gè)應(yīng)用層該解決的問(wèn)題。所以,數(shù)據(jù)相關(guān)的計(jì)算應(yīng)該通過(guò) UITableView 的委托來(lái)解決。這樣可以讓 UITableView 和數(shù)據(jù)層分別獨(dú)立。視圖層就負(fù)責(zé)顯示數(shù)據(jù),你遞過(guò)來(lái)什么我就顯示什么。

下面這張圖很好的解釋了 UITableView 的工作過(guò)程:

UITableView 的工作僅僅是展示數(shù)據(jù),但是最終它需要知道自己要展示那些數(shù)據(jù),這時(shí)就可以向它的委托詢(xún)問(wèn)。在 objc 的委托模式里,一個(gè)類(lèi)可以通過(guò)協(xié)議來(lái)聲明可選或者必須的方法。

看起來(lái)似乎繼承然后重寫(xiě)必須的方法來(lái)的更簡(jiǎn)單一點(diǎn)。但是考慮一下這個(gè)問(wèn)題:繼承的結(jié)果必定是一個(gè)獨(dú)立的類(lèi),如果你想讓某個(gè)對(duì)象成為多個(gè)對(duì)象的委托,那么子類(lèi)這招就行不通了。

注意:委托模式十分重要,蘋(píng)果在 UIKit 中大量使用了該模式,基本上隨處可見(jiàn)。

如何使用委托模式

打開(kāi) 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!)
}

對(duì)上面三個(gè)部分進(jìn)行拆解:

  1. 關(guān)閉導(dǎo)航欄的透明效果

  2. 通過(guò) API 獲取所有的專(zhuān)輯數(shù)據(jù),記住,我們使用外觀模式之后,應(yīng)該從 LibraryAPI 獲取數(shù)據(jù),而不是 PersistencyManager

  3. 你可以在這里設(shè)置你的 UITablweView ,在這里聲明了 UITableViewdelegate 是當(dāng)前的 ViewController 。事實(shí)上你用了 XIB 或者 StoryBoard ,可以直接在可視化的頁(yè)面里拖拽完成。

接下來(lái)添加一個(gè)新的方法用來(lái)更方便的獲取數(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() 這個(gè)方法獲取最新的專(zhuān)輯數(shù)據(jù),當(dāng)你想要展示新數(shù)據(jù)的時(shí)候,你需要調(diào)用 reloadData() 這個(gè)方法,這樣 UITableView 就會(huì)向委托請(qǐng)求數(shù)據(jù),比如有多少個(gè) section 有多少個(gè) row 之類(lèi)的。

viewDidLoad 里面調(diào)用上面的方法:

self.showDataForAlbum(currentAlbumIndex)

這樣應(yīng)用一啟動(dòng)就會(huì)去加載當(dāng)前的專(zhuān)輯數(shù)據(jù)。因?yàn)?currentAlbumIndex 的默認(rèn)值是 0 ,所以一開(kāi)始會(huì)默認(rèn)顯示第一章專(zhuān)輯的信息。

接下來(lái)我們?cè)撊ネ晟?DataSource 的協(xié)議方法了。你可以直接把委托方法寫(xiě)在類(lèi)里面,當(dāng)然如果你想讓你的代碼看起來(lái)更整潔一點(diǎn),則可以放在擴(kuò)展里。

在文件底部添加如下方法,注意一定要放在類(lèi)定義的大括號(hào)外面,因?yàn)檫@兩個(gè)家伙不是類(lèi)定義的一部分,它們是擴(kuò)展:

extension ViewController: UITableViewDataSource {
}

extension ViewController: UITableViewDelegate {
}

上面就是實(shí)現(xiàn)委托的方法 - 你可以把協(xié)議想象成是與委托之間的約定,只要你實(shí)現(xiàn)了約定的方法,就算是實(shí)現(xiàn)了委托。在我們的代碼中, ViewController 需要遵守 UITableViewDataSourceUITableViewDelegate 的協(xié)議。這樣 UITableView 才能確保必要的委托方法都已經(jīng)實(shí)現(xiàn)了。

UITableViewDataSource 對(duì)應(yīng)的那個(gè)擴(kuò)展里加上如下方法:

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ù),和存儲(chǔ)的數(shù)據(jù)中的 title 的數(shù)目相同。

tableView(_:cellForRowAtIndexPath:) 創(chuàng)建并且返回了一個(gè)單元格,上面有標(biāo)題和對(duì)應(yīng)的值。

注意:你可以把這些方法直接加在類(lèi)聲明里面,也可以放在擴(kuò)展里,編譯器不會(huì)去管數(shù)據(jù)源到底在哪里,只要能找到對(duì)應(yīng)的方法就可以了。而我們之所以這樣做,是為了方便其他人閱讀。

此時(shí)再構(gòu)建項(xiàng)目,你可以看到如下內(nèi)容:

是的,顯示成功啦!目前的項(xiàng)目源碼在這里:BlueLibrarySwift-Part1,如果遇到什么問(wèn)題你可以下載下來(lái)對(duì)比一下。

下一章我們會(huì)繼續(xù)設(shè)計(jì)模式的內(nèi)容,敬請(qǐng)期待!

以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)