IOS設計模式之二(facade門面模式,裝飾器模式)
門面模式針對複雜的子系統提供了單一的接口,不需要暴漏一些列的類和API給用戶,你僅僅暴漏一個簡單統一的API。
下面的圖解釋了這個概念:
這個API的使用者完全不需要關心背後的複雜性。這個模式非常適合有一大堆很難使用或者理解的類的情況。
門面模式解耦了使用系統的程式碼和需要隱藏的接口和實現類。它也降低了外部程式碼對內部子系統的依賴性。當隱藏在門面之後的類很容易發生變化的時候,此模式就很有用了,因為當背後的類發生變化的時候,門麵類始終保持了同樣的API。
舉個例子來說,如果有一天你想取代後端服務,你不需要改變API的使用者,因為API沒有發生變化。
如何使用門面模式
當前你已經用PersistencyManager本地保存專輯數據,使用HTTPClient處理遠程連接,工程中的其它類暫時與本次實現的邏輯無關。
為了實現這個模式,只有LibraryAPI應該保存PersistencyManager和HTTPClient的實例,然後LibraryAPI將暴漏一個簡單的API去訪問這些服務。
注意: 通常來說,單例類的生命週期貫穿於整個應用的生命週期中,你不應對保存太多其它對象的強引用,因為他們只有到應用關閉的時候才能被釋放。
本次設計看起來像下圖:
LibraryAPI將暴漏給其它程式碼,但是它隱藏了HTTPClient和PersistencyManager的複雜性。
打開LibraryAPI.h,在文件頭部增加下面的導入語句:
- #import "Album.h"
接下來,在LibraryAPI.h中增加如下的方法定義:
- - (NSArray*)getAlbums;
- - (void)addAlbum:(Album*)album atIndex:(int)index;
- - (void)deleteAlbumAtIndex:(int)index;
目前有一些你需要暴漏給其它類的方法。
打開LibraryAPI.m,增加如下的兩個導入語句:
- #import "PersistencyManager.h"
- #import "HTTPClient.h"
這裡將是唯一的導入這兩個類的地方。記住:你的API是對於復雜系統唯一的訪問點。
現在,增加通過類擴展(class extension)增加一些私有的變數(在@implementation 行之上):
- @interfaceLibraryAPI () {
- PersistencyManager *persistencyManager;
- HTTPClient *httpClient;
- BOOL isOnline;
- }
- @end
isOnline決定了是否服務器中任何專輯資料的改變應該被更新,例如增加或者刪除專輯。
你現在需要通過init初始化這些變數。在LibraryAPI.m中增加如下的程式碼:
- - (id)init{
- self = [super init];
- if (self) {
- persistencyManager = [[PersistencyManager alloc] init];
- httpClient = [[HTTPClient alloc] init];
- isOnline = NO;
- }
- return self;
- }
HTTP 客戶端實際上不會真正的和一個服務器溝通,它在這裡僅僅是用來演示門面模式的使用,所以isOnline將總是NO。
接下來,增加如下的三個方法到LibraryAPI.m:
- -(NSArray*)getAlbums
- {
- return [persistencyManager getAlbums];
- }
- - (void)addAlbum:(Album*)album atIndex:(int)index
- {
- [persistencyManager addAlbum:album atIndex:index];
- if (isOnline)
- {
- [httpClient postRequest:@"/api/addAlbum" body:[album description]];
- }
- }
- - (void)deleteAlbumAtIndex:(int)index
- {
- [persistencyManager deleteAlbumAtIndex:index];
- if (isOnline)
- {
- [httpClient postRequest:@"/api/deleteAlbum" body:[@(index) description]];
- }
- }
我們來看一看addAlbum:atIndex:.這個類首先更新本地的數據,然後如果有網絡連接,它更新遠程服務器。這就是門面模式的強大之處。當某些外部的類增加一個新的專輯的時候,它不知道也不需要知道背後的複雜性。
注意:當為子系統的類設計門面的時候,要記住:任何東西都不能阻止客戶端直接訪問這些隱藏的類。不要對這些防禦性的程式碼太過於吝嗇,並且也不要假設所有的客戶端都會和門面一樣使用你的類。
構建並運行你的應用。你將看到一個激動人心的空白的黑屏(哈哈):
接下來,你將需要在屏幕上顯示專輯數據,使用你的下個設計模式-裝飾器設計模式將是非常好的選擇。
裝飾器(Decorator)模式
裝飾器模式在不修改原來程式碼的情況下動態的給對象增加新的行為和職責,它通過一個對象包裝被裝飾對象的方法來修改類的行為,這種方法可以做為子類化的一種替代方法。
在Objective-C中,存在兩種非常常見的實現:Category(類別)和Delegation(委託)。
Category(類別)
Category(類別)是一種不需要子類化就可以讓你能動態的給已經存在的類增加方法的強有力的機制。新增的方法是在編譯期增加的,這些方法執行的時候和被擴展的類的其它方法是一樣的。它可能與裝飾器設計模式的定義稍微有點不同,因為Category(類別)不會保存被擴展類的引用。
注意: 你除了可以擴展你自己的類以外,你還可以給Cocoa自己的類增加方法。
如何使用類別
設想一種情況,你需要讓Album(專輯)對象顯示在一個表格視圖(TableView)中:
專輯的標題從何而來?因為專輯是模型對象,它本身不需要關心你如何顯示它的數據。你需要增加一些程式碼去擴展專輯類的行為,但是不需要直接修改專輯類。
你將創建一個專輯類擴展的類別,它將定義一個新的方法,這個方法會返回能很容易和UITableViews使用的數據結構。這個數據結構如下圖所示:
為了給Album增加一個類別,導航到“File\New\File...\",選擇Objective-C category模板,不要習慣性的選擇Objective-C class模板。在Category域輸入TableRepresentation,Category on域輸入Album.
注意:你已經註意到了新建文件的名字了嗎? Album+TableRepresentation意味著你正在擴展Album類。這種約定是非常重要,因為它方便閱讀以及阻止和你或者其他人創建的類別衝突。
打開Album+TableRepresentation.h類,新增如下的方法原型:
- - (NSDictionary*)tr_tableRepresentation;
注意在方法開頭有一個tr_前綴,它是類別TableRepresentation的縮寫。再一次,這種約定也可以阻止和其它的方法衝突。
注意:如果方法與原來類的方法重名了,或者與同樣的類(甚至它的父類)的其它的擴展重名,那麼運行期到底應該調用哪個方法是未定義的。當你僅僅是在擴展你自己寫的類時,這沒什麼問題, 但是當你在擴展標準的Cocoa 或者Cocoa Touch類的時候,它可能會導致嚴重的問題。
打開Album+TableRepresentation.m,增加下面的方法:
- - (NSDictionary*)tr_tableRepresentation
- {
- return @{@"titles":@[@"Artist", @"Album", @"Genre", @"Year"],
- @"values":@[self.artist, self.title, self.genre, self.year]};
- }
我們稍停片刻來看看這個模式的強大之處:
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的時候會發生什麼:
UITableView的職責就是顯示一個表格視圖。然而最終它需要一些它自身沒有的信息。那麼它就求助於它的委託,通過發送消息給委託來獲取信息。在Objective-C實現委託模式的時候,一個類可以通過協議(Protocol)來聲明可選以及必要的方法。本指南稍後會涉及協議方面的內容。
子類化一個對象,複寫需要的方法看起來好像更容易一點,但是考慮到你只能子類化一個類,如果你想一個對像作為兩個或者更多對象的委託的話,使用子類化將不能實現。
注意:這個是一個重要的模式。蘋果在UIKit類中大量使用了它:UITableView, UITextView,UITextField, UIWebView, UIAlert, UIActionSheet, UICollectionView,UIPickerView,UIGestureRecognizer, UIScrollView等等等。
如何使用委托模式
打開ViewController.m文件,在文件開頭增加下面的導入語句:
- #import "LibraryAPI.h"
- #import "Album+TableRepresentation.h"
現在,給類的擴展增加如下的私有變數,最終類的擴展如下所示:
- @interfaceViewController () {
- UITableView *dataTable;
- NSArray *allAlbums;
- NSDictionary *currentAlbumData;
- int currentAlbumIndex;
- }
- @end
然後用下面的程式碼取代類型擴展中@interface一行:
- @interface ViewController () <UITableViewDataSource, UITableViewDelegate> {
這就是如何使得委託符合協議,你可以把它認為是委託履行協議方法契約的約定。在這裡,你指定ViewController將實現UITableViewDataSource和UITableViewDelegate協議。這種方式使得UITableView非常確定那些委託必須要實現的方法。
接下來,用如下程式碼取代viewDidLoad方法:
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- // 1
- self.view.backgroundColor = [UIColor colorWithRed:0.76f green:0.81f blue:0.87f alpha:1];
- currentAlbumIndex = 0;
- //2
- allAlbums = [[LibraryAPI sharedInstance] getAlbums];
- // 3
- // the uitableview that presents the album data
- dataTable = [[UITableView alloc] initWithFrame:CGRectMake(0, 120, self.view.frame.size.width, self.view.frame.size.height-120) style:UITableViewStyleGrouped];
- dataTable.delegate = self;
- dataTable.dataSource = self;
- dataTable.backgroundView = nil;
- [self.view addSubview:dataTable];
- }
下面我們來解釋一下上面程式碼中標記了數字的地方:
1. 改變背景色為漂亮的藏青色
2. 通過API獲取專輯數據。你不需要直接使用PersistencyManager。
3. 創建UITableView,聲明viewController為UITableView的委託和數據源;因此viewController將提供所有的被UITableView需要的信息。
現在,在ViewController.m中增加如下的方法:
- - (void)showDataForAlbumAtIndex:(int)albumIndex
- {
- // defensive code: make sure the requested index is lower than the amount of albums
- if (albumIndex < allAlbums.count)
- {
- // fetch the album
- Album *album = allAlbums[albumIndex];
- // save the albums data to present it later in the tableview
- currentAlbumData = [album tr_tableRepresentation];
- }
- else
- {
- currentAlbumData = nil;
- }
- // we have the data we need, let's refresh our tableview
- [dataTable reloadData];
- }
showDataForAlbumAtIndex:從專輯數組中獲取需要的專輯數據。當你想去顯示新的數據的時候,你僅僅需要調用reloadData.這將使得UITableView去問委託一些如下的信息:表格視圖有多少個區域,每個區域應該顯示多少行,每個單元格長什麼樣。
在viewDidLoad最後增加下面一行程式碼:
- [self showDataForAlbumAtIndex:currentAlbumIndex];
上面的程式碼會在app啟動的時候加載當前的專輯,因為currentAlbumIndex設置為0,所以它將顯示第一個專輯。
構建並運行你的工程;你將得到一個崩潰信息,在調試控制台上面將顯示如下的異常:
這裡出了什麼問題?你聲明ViewController作為UITableView的委託和數據源,但是這樣做了,也就意味著你必須實現所必須的方法-這些方法包括你還沒有實現的tableView:numberOfRowsInSection:方法.
在ViewController.m文件中@implementation 和@end 之間的任何位置,增加下面的程式碼:
- - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- {
- return [currentAlbumData[@"titles"] count];
- }
- - (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
- if (!cell)
- {
- cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"];
- }
- cell.textLabel.text = currentAlbumData[@"titles"][indexPath.row];
- cell.detailTextLabel.text = currentAlbumData[@"values"][indexPath.row];
- return cell;
- }
tableView:numberOfRowsInSection:方法返回表格視圖需要顯示的行數。這個和數據結構中的標題數量是匹配的。
tableView:cellForRowAtIndexPath:創建並返回一個包含標題和標題值的的單元格。
構建並運行你的工程,你的app應該會正常啟動並顯示下圖所示的畫面:
到目前為止進展挺順利。但是你回憶第一張本app最終效果的圖,你會發現在屏幕頂部有一個水平滾動視圖在不同的專輯之間切換。並不是構建一個只為這次使用的單一目的的水平滾動視圖,你為什麼不做一個可以讓任何視圖復用的滾動視圖呢?
為了使這個視圖可以復用,應該由委託來決定所有的從左邊開始依次到下一個對象的內容。水平滾動條應該聲明那些能和它一起工作的委託方法,這有點類似UITableView的委託方法的方式。我們將在討論下一個模式的時候來實現它。
繼續觀看:
繼續觀看:
沒有留言:
張貼留言