iOS設計模式之二:外觀模式和裝飾器模式

發表於2013-09-17

外觀(Facade)模式

外觀模式針對複雜的子系統提供了單一的介面,不需要暴露一些列的類和API給使用者,你僅僅公開一個簡單統一的API。下面的圖解釋了這個概念:
IOSDesign7

這個API的使用者完全不需要關心背後的複雜性。這個模式非常適合有一大堆很難使用或者理解的類的情況。外觀模式解耦了使用系統的程式碼和需要隱藏的介面和實現類。它也降低了外部程式碼對內部子系統的依賴性。當隱藏在外觀之後的類很容易發生變化的時候,此模式就很有用了,因為當背後的類發生變化的時候,外觀類始終保持了同樣的API。

舉個例子來說,如果有一天你想取代後端服務,你不需要改變API的使用者,因為API沒有發生變化。

如何使用外觀模式

當前你已經用PersistencyManager本地儲存專輯資料,使用HTTPClient處理遠端連線,工程中的其它類暫時與本次實現的邏輯無關。為了實現這個模式,只有LibraryAPI應該儲存PersistencyManager和HTTPClient的例項,然後LibraryAPI將暴漏一個簡單的API去訪問這些服務。

注意:通常來說,單例類的生命週期貫穿於整個應用的生命週期中,你不應對儲存太多其它物件的強引用,因為他們只有到應用關閉的時候才能被釋放。

本次設計看起來像下圖:

IOSDesign8
LibraryAPI將暴漏給其它程式碼,但是它隱藏了HTTPClient和PersistencyManager的複雜性。

開啟LibraryAPI.h,在檔案頭部增加下面的匯入語句:


接下來,在LibraryAPI.h中增加如下的方法定義:

目前有一些你需要暴漏給其它類的方法。開啟LibraryAPI.m,增加如下的兩個匯入語句:

這裡將是唯一的匯入這兩個類的地方。記住:你的API是對於複雜系統唯一的訪問點。現在,增加通過類擴充套件(class extension)增加一些私有的變數(在@implementation行之上):

isOnline決定了是否伺服器中任何專輯資料的改變應該被更新,例如增加或者刪除專輯。你現在需要通過init初始化這些變數。在LibraryAPI.m中增加如下的程式碼:

HTTP客戶端實際上不會真正的和一個伺服器互動,它在這裡僅僅是用來演示外觀模式的使用,所以isOnline將總是NO。

接下來,增加如下的三個方法到LibraryAPI.m:


我們來看一看addAlbum:atIndex:.這個類首先更新本地的資料,然後如果有網路連線,它更新遠端伺服器。這就是外觀模式的強大之處。當某些外部的類增加一個新的專輯的時候,它不知道也不需要知道背後的複雜性。

注意:當為子系統的類設計外觀的時候,要記住:任何東西都不能阻止客戶端直接訪問這些隱藏的類。不要對這些防禦性的程式碼太過於吝嗇,並且也不要假設所有的客戶端都會和外觀一樣使用你的類。

構建並執行你的應用。你將看到一個激動人心的空白的黑屏(哈哈):

IOSDesign9
接下來,你將需要在螢幕上顯示專輯資料,使用你的下個設計模式-裝飾器設計模式將是非常好的選擇。
裝飾器(Decorator)模式

裝飾器模式在不修改原來程式碼的情況下動態的給物件增加新的行為和職責,它通過一個物件包裝被裝飾物件的方法來修改類的行為,這種方法可以做為子類化的一種替代方法。

在Objective-C中,存在兩種非常常見的實現:Category(類別)和Delegation(委託)。
Category(類別)
Category(類別)是一種不需要子類化就可以讓你能動態的給已經存在的類增加方法的強有力的機制。新增的方法是在編譯期增加的,這些方法執行的時候和被擴充套件的類的其它方法是一樣的。它可能與裝飾器設計模式的定義稍微有點不同,因為Category(類別)不會儲存被擴充套件類的引用。

注意:你除了可以擴充套件你自己的類以外,你還可以給Cocoa自己的類增加方法。

如何使用類別

設想一種情況,你需要讓Album(專輯)物件顯示在一個表格檢視(TableView)中:

IOSDesign10
專輯的標題從何而來?因為專輯是模型物件,它本身不需要關心你如何顯示它的資料。你需要增加一些程式碼去擴充套件專輯類的行為,但是不需要直接修改專輯類。

你將建立一個專輯類擴充套件的類別,它將定義一個新的方法,這個方法會返回能很容易和UITableViews使用的資料結構。這個資料結構如下圖所示:

IOSDesign11
為了給Album增加一個類別,導航到“File\New\File…\”,選擇Objective-C category模板,不要習慣性的選擇Objective-C class模板。在Category域輸入TableRepresentation,Category on域輸入Album.

注意:你已經注意到了新建檔案的名字了嗎?Album+TableRepresentation意味著你正在擴充套件Album類。這種約定是非常重要,因為它方便閱讀以及阻止和你或者其他人建立的類別衝突。

開啟Album+TableRepresentation.h類,新增如下的方法原型:


注意在方法開頭有一個tr_字首,它是類別TableRepresentation的縮寫。再一次,這種約定也可以阻止和其它的方法衝突。

注意:如果方法與原來類的方法重名了,或者與同樣的類(甚至它的父類)的其它的擴充套件重名,那麼執行期到底應該呼叫哪個方法是未定義的。當你僅僅是在擴充套件你自己寫的類時,這沒什麼問題,但是當你在擴充套件標準的Cocoa或者Cocoa Touch類的時候,它可能會導致嚴重的問題。
開啟Album+TableRepresentation.m,增加下面的方法:


我們們稍停片刻,來看看這個模式的強大之處:
  • 1.你可以直接使用Album的屬性
  • 2.你不需要子類化就可以增加方法。當然如果你想子類化Album,你任然可以那樣做。
  • 3.簡簡單單的幾句程式碼就讓你在不修改Album的情況下,返回了一個UITableView風格的Album。

蘋果在Foundation類中大量使用了Category。想知道他們是怎麼做的,你可以代開NSString.h檔案,找到@interface NSString,你將看到類和其它三個類別的定義:NSStringExtensionMethods、NSExtendedStringPropertyListParsing、NSStringDeprecated.類別讓方法組織成相互獨立的部分。

Delegation(委託)

委託作為另外一個裝飾器模式,它是一種和其它物件互動的機制。舉例來說,當你使用UITableView的時候,你必須要實現tableView:numberOfRowsInSection:方法。

你不可能讓UITableView知道它需要在每個區域顯示多少行,因為這些是應用特定的資料。因此計算每個區域需要顯示多少行的職責就給了UITableView的委託。這就讓UITableView類獨立於它要顯示的資料。

這裡通過一個圖來解釋當你建立UITableView的時候會發生什麼:

IOSDesign12

UITableView的職責就是顯示一個表格檢視。然而最終它需要一些它自身沒有的資訊。那麼它就求助於它的委託,通過傳送訊息給委託來獲取資訊。在Objective-C實現委託模式的時候,一個類可以通過協議(Protocol)來宣告可選以及必要的方法。本指南稍後會涉及協議方面的內容。

子類化一個物件,複寫需要的方法看起來好像更容易一點,但是考慮到你只能子類化一個類,如果你想一個物件作為兩個或者更多物件的委託的話,使用子類化將不能實現。

注意:這個是一個重要的模式。蘋果在UIKit類中大量使用了它:UITableView、UITextView、UITextField、UIWebView、UIAlert、UIActionSheet、UICollectionView,、UIPickerView、UIGestureRecognizer,、UIScrollView 等等等。
如何使用委託模式

開啟ViewController.m檔案,在檔案開頭增加下面的匯入語句:


現在,給類的擴充套件增加如下的私有變數,最終類的擴充套件如下所示:

然後用下面的程式碼取代型別擴充套件中@interface一行:

這就是如何使得委託符合協議,你可以把它認為是委託履行協議方法契約的約定。在這裡,你指定ViewController將實現UITableViewDataSource和UITableViewDelegate協議。這種方式使得UITableView非常確定那些委託必須要實現的方法。

接下來,用如下程式碼取代viewDidLoad方法:


下面我們來解釋一下上面程式碼中標記了數字的地方:
  • 1.改變背景色為漂亮的藏青色
  • 2.通過API獲取專輯資料。你不需要直接使用PersistencyManager。
  • 3.建立UITableView,宣告viewController為UITableView的委託和資料來源;因此viewController將提供所有的被UITableView需要的資訊。

現在,在ViewController.m中增加如下的方法:


showDataForAlbumAtIndex:從專輯陣列中獲取需要的專輯資料。當你想去顯示新的資料的時候,你僅僅需要呼叫reloadData.這將使得UITableView去問委託一些如下的資訊:表格檢視有多少個區域,每個區域應該顯示多少行,每個單元格長什麼樣。

在viewDidLoad最後增加下面一行程式碼:


上面的程式碼會在app啟動的時候載入當前的專輯,因為currentAlbumIndex設定為0,所以它將顯示第一個專輯。

構建並執行你的工程;你將得到一個崩潰資訊,在除錯控制檯上面將顯示如下的異常:

IOSDesign13

這裡出了什麼問題?你宣告ViewController作為UITableView的委託和資料來源,但是這樣做了,也就意味著你必須實現所必須的方法-這些方法包括你還沒有實現的tableView:numberOfRowsInSection:方法.

在ViewController.m檔案中@implementation和@end之間的任何位置,增加下面的程式碼:


tableView:numberOfRowsInSection:方法返回表格檢視需要顯示的行數。這個和資料結構中的標題數量是匹配的。tableView:cellForRowAtIndexPath:建立並返回一個包含標題和標題值的的單元格。

構建並執行你的工程,你的app應該會正常啟動並顯示下圖所示的畫面:

IOSDesign14
到目前為止進展挺順利。但是你回憶第一張本app最終效果的圖,你會發現在螢幕頂部有一個水平滾動檢視在不同的專輯之間切換。並不是構建一個只為這次使用的單一目的的水平滾動檢視,你為什麼不做一個可以讓任何檢視複用的滾動檢視呢?

為了使這個檢視可以複用,應該由委託來決定所有的從左邊開始依次到下一個物件的內容。水平滾動條應該宣告那些能和它一起工作的委託方法,這有點類似UITableView的委託方法的方式。我們將在討論下一個模式的時候來實現它。

相關文章